From 5b69c53334b573fa1752a7c48c3d41eae675eb59 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Tue, 19 Jan 2021 14:31:30 +0100 Subject: [PATCH 001/101] Setup project for local development (#4753) Signed-off-by: Thomas Quandt --- src/jetstream/config.dev | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jetstream/config.dev b/src/jetstream/config.dev index dc5aca85cf..43e6816d0c 100644 --- a/src/jetstream/config.dev +++ b/src/jetstream/config.dev @@ -12,7 +12,7 @@ SKIP_SSL_VALIDATION=true CONSOLE_PROXY_TLS_ADDRESS=:5443 CONSOLE_CLIENT=console CF_CLIENT=cf -UAA_ENDPOINT= +UAA_ENDPOINT=http://localhost:8080 CONSOLE_ADMIN_SCOPE=stratos.admin CF_ADMIN_ROLE=cloud_controller.admin ALLOWED_ORIGINS=http://nginx @@ -52,7 +52,7 @@ INVITE_USER_CLIENT_ID= INVITE_USER_CLIENT_SECRET= # Use local admin user rather than UAA users -AUTH_ENDPOINT_TYPE=local +AUTH_ENDPOINT_TYPE=remote LOCAL_USER=admin LOCAL_USER_PASSWORD=admin LOCAL_USER_SCOPE=stratos.admin From e6675570eb81ad5376c958cee2ad9d555d7f3fb3 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Tue, 19 Jan 2021 14:39:38 +0100 Subject: [PATCH 002/101] Allow everyone to add endpoints in the front-end for testing purposes (#4753) Needs to be properly implemented later. Signed-off-by: Thomas Quandt --- .../core/permissions/stratos-user-permissions.checker.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 e3410a2a1f..294ba40f5b 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 @@ -35,12 +35,12 @@ export enum StratosPermissionStrings { STRATOS_ADMIN = 'isAdmin' } - export enum StratosScopeStrings { STRATOS_CHANGE_PASSWORD = 'password.write', SCIM_READ = 'scim.read', SCIM_WRITE = 'scim.write', - STRATOS_NOAUTH = 'stratos.noauth' + STRATOS_NOAUTH = 'stratos.noauth', + STRATOS_ENDPOINTADMIN = 'stratos.user' //TODO needs to be implemented properly } export enum StratosPermissionTypes { @@ -54,8 +54,8 @@ export enum StratosPermissionTypes { // true (see `getCheckFromConfig`). export const stratosPermissionConfigs: IPermissionConfigs = { [StratosCurrentUserPermissions.ENDPOINT_REGISTER]: new PermissionConfig( - StratosPermissionTypes.STRATOS, - StratosPermissionStrings.STRATOS_ADMIN + StratosPermissionTypes.STRATOS_SCOPE, + StratosScopeStrings.STRATOS_ENDPOINTADMIN ), [StratosCurrentUserPermissions.PASSWORD_CHANGE]: new PermissionConfig( StratosPermissionTypes.STRATOS_SCOPE, From a88bba608346606a67625a30b5de7f2bf7d5b1d3 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Tue, 19 Jan 2021 15:30:18 +0100 Subject: [PATCH 003/101] Add "created_by" to the cnsis table (#4753) Adjust files to insert the userID to an endpoint when it's created. Signed-off-by: Thomas Quandt --- src/jetstream/cnsi.go | 15 ++++++++++-- .../datastore/20210119150000_CreatedBy.go | 20 ++++++++++++++++ src/jetstream/plugins/cloudfoundry/main.go | 12 +++++----- src/jetstream/repository/cnsis/pgsql_cnsis.go | 24 +++++++++++-------- .../repository/interfaces/portal_proxy.go | 2 +- .../repository/interfaces/structs.go | 2 ++ 6 files changed, 56 insertions(+), 19 deletions(-) create mode 100644 src/jetstream/datastore/20210119150000_CreatedBy.go diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 0952daba1a..6986833f5a 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -67,7 +67,17 @@ func (p *portalProxy) RegisterEndpoint(c echo.Context, fetchInfo interfaces.Info cnsiClientSecret = p.GetConfig().CFClientSecret } - newCNSI, err := p.DoRegisterEndpoint(params.CNSIName, params.APIEndpoint, skipSSLValidation, cnsiClientId, cnsiClientSecret, ssoAllowed, subType, fetchInfo) + sessionValue, err := p.GetSessionValue(c, "user_id") + if err != nil { + //maybe better to just return error? + return interfaces.NewHTTPShadowError( + http.StatusInternalServerError, + "Failed to get session user", + "Failed to get session user: %v", err) + } + uaaUserId := sessionValue.(string) + + newCNSI, err := p.DoRegisterEndpoint(params.CNSIName, params.APIEndpoint, skipSSLValidation, cnsiClientId, cnsiClientSecret, uaaUserId, ssoAllowed, subType, fetchInfo) if err != nil { return err } @@ -76,7 +86,7 @@ func (p *portalProxy) RegisterEndpoint(c echo.Context, fetchInfo interfaces.Info return nil } -func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, skipSSLValidation bool, clientId string, clientSecret string, ssoAllowed bool, subType string, fetchInfo interfaces.InfoFunc) (interfaces.CNSIRecord, error) { +func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, skipSSLValidation bool, clientId string, clientSecret string, userId string, ssoAllowed bool, subType string, fetchInfo interfaces.InfoFunc) (interfaces.CNSIRecord, error) { if len(cnsiName) == 0 || len(apiEndpoint) == 0 { return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( @@ -134,6 +144,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk newCNSI.ClientSecret = clientSecret newCNSI.SSOAllowed = ssoAllowed newCNSI.SubType = subType + newCNSI.CreatedBy = userId err = p.setCNSIRecord(guid, newCNSI) diff --git a/src/jetstream/datastore/20210119150000_CreatedBy.go b/src/jetstream/datastore/20210119150000_CreatedBy.go new file mode 100644 index 0000000000..b3e711ad7c --- /dev/null +++ b/src/jetstream/datastore/20210119150000_CreatedBy.go @@ -0,0 +1,20 @@ +package datastore + +import ( + "database/sql" + + "bitbucket.org/liamstask/goose/lib/goose" +) + +func init() { + RegisterMigration(20210119150000, "CreatedBy", func(txn *sql.Tx, conf *goose.DBConf) error { + alterCNSI := "ALTER TABLE cnsis ADD COLUMN created_by VARCHAR(36) NOT NULL DEFAULT '';" + + _, err := txn.Exec(alterCNSI) + if err != nil { + return err + } + + return nil + }) +} diff --git a/src/jetstream/plugins/cloudfoundry/main.go b/src/jetstream/plugins/cloudfoundry/main.go index 10a8153904..59c7e96e7d 100644 --- a/src/jetstream/plugins/cloudfoundry/main.go +++ b/src/jetstream/plugins/cloudfoundry/main.go @@ -109,6 +109,11 @@ func (c *CloudFoundrySpecification) cfLoginHook(context echo.Context) error { return nil } + userGUID, err := c.portalProxy.GetSessionStringValue(context, "user_id") + if err != nil { + return fmt.Errorf("Could not determine user_id from session: %s", err) + } + // CF auto reg cnsi entry missing, attempt to register if cfCnsi.CNSIType == "" { cfEndpointSpec, _ := c.portalProxy.GetEndpointTypeSpec("cf") @@ -122,7 +127,7 @@ func (c *CloudFoundrySpecification) cfLoginHook(context echo.Context) error { log.Infof("Auto-registering cloud foundry endpoint %s as \"%s\"", cfAPI, autoRegName) // Auto-register the Cloud Foundry - cfCnsi, err = c.portalProxy.DoRegisterEndpoint(autoRegName, cfAPI, true, c.portalProxy.GetConfig().CFClient, c.portalProxy.GetConfig().CFClientSecret, false, "", cfEndpointSpec.Info) + cfCnsi, err = c.portalProxy.DoRegisterEndpoint(autoRegName, cfAPI, true, c.portalProxy.GetConfig().CFClient, c.portalProxy.GetConfig().CFClientSecret, userGUID, false, "", cfEndpointSpec.Info) if err != nil { log.Errorf("Could not auto-register Cloud Foundry endpoint: %v", err) return nil @@ -138,11 +143,6 @@ func (c *CloudFoundrySpecification) cfLoginHook(context echo.Context) error { log.Infof("Determining if user should auto-connect to %s.", cfAPI) - userGUID, err := c.portalProxy.GetSessionStringValue(context, "user_id") - if err != nil { - return fmt.Errorf("Could not determine user_id from session: %s", err) - } - cfTokenRecord, ok := c.portalProxy.GetCNSITokenRecordWithDisconnected(cfCnsi.GUID, userGUID) if ok && cfTokenRecord.Disconnected { // There exists a record but it's been cleared. This means user has disconnected manually. Don't auto-reconnect diff --git a/src/jetstream/repository/cnsis/pgsql_cnsis.go b/src/jetstream/repository/cnsis/pgsql_cnsis.go index 03116f004c..393559d147 100644 --- a/src/jetstream/repository/cnsis/pgsql_cnsis.go +++ b/src/jetstream/repository/cnsis/pgsql_cnsis.go @@ -2,6 +2,7 @@ package cnsis import ( "database/sql" + "encoding/json" "errors" "fmt" "net/url" @@ -12,23 +13,23 @@ import ( log "github.com/sirupsen/logrus" ) -var listCNSIs = `SELECT guid, name, cnsi_type, api_endpoint, auth_endpoint, token_endpoint, doppler_logging_endpoint, skip_ssl_validation, client_id, client_secret, sso_allowed, sub_type, meta_data +var listCNSIs = `SELECT guid, name, cnsi_type, api_endpoint, auth_endpoint, token_endpoint, doppler_logging_endpoint, skip_ssl_validation, client_id, client_secret, sso_allowed, sub_type, meta_data, created_by FROM cnsis` -var listCNSIsByUser = `SELECT c.guid, c.name, c.cnsi_type, c.api_endpoint, c.doppler_logging_endpoint, t.user_guid, t.token_expiry, c.skip_ssl_validation, t.disconnected, t.meta_data, c.sub_type, c.meta_data as endpoint_metadata +var listCNSIsByUser = `SELECT c.guid, c.name, c.cnsi_type, c.api_endpoint, c.doppler_logging_endpoint, t.user_guid, t.token_expiry, c.skip_ssl_validation, t.disconnected, t.meta_data, c.sub_type, c.meta_data as endpoint_metadata, c.created_by FROM cnsis c, tokens t WHERE c.guid = t.cnsi_guid AND t.token_type=$1 AND t.user_guid=$2 AND t.disconnected = '0'` -var findCNSI = `SELECT guid, name, cnsi_type, api_endpoint, auth_endpoint, token_endpoint, doppler_logging_endpoint, skip_ssl_validation, client_id, client_secret, sso_allowed, sub_type, meta_data +var findCNSI = `SELECT guid, name, cnsi_type, api_endpoint, auth_endpoint, token_endpoint, doppler_logging_endpoint, skip_ssl_validation, client_id, client_secret, sso_allowed, sub_type, meta_data, created_by FROM cnsis WHERE guid=$1` -var findCNSIByAPIEndpoint = `SELECT guid, name, cnsi_type, api_endpoint, auth_endpoint, token_endpoint, doppler_logging_endpoint, skip_ssl_validation, client_id, client_secret, sso_allowed, sub_type, meta_data +var findCNSIByAPIEndpoint = `SELECT guid, name, cnsi_type, api_endpoint, auth_endpoint, token_endpoint, doppler_logging_endpoint, skip_ssl_validation, client_id, client_secret, sso_allowed, sub_type, meta_data, created_by FROM cnsis WHERE api_endpoint=$1` -var saveCNSI = `INSERT INTO cnsis (guid, name, cnsi_type, api_endpoint, auth_endpoint, token_endpoint, doppler_logging_endpoint, skip_ssl_validation, client_id, client_secret, sso_allowed, sub_type, meta_data) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)` +var saveCNSI = `INSERT INTO cnsis (guid, name, cnsi_type, api_endpoint, auth_endpoint, token_endpoint, doppler_logging_endpoint, skip_ssl_validation, client_id, client_secret, sso_allowed, sub_type, meta_data, created_by) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)` var deleteCNSI = `DELETE FROM cnsis WHERE guid = $1` @@ -87,7 +88,7 @@ func (p *PostgresCNSIRepository) List(encryptionKey []byte) ([]*interfaces.CNSIR cnsi := new(interfaces.CNSIRecord) - err := rows.Scan(&cnsi.GUID, &cnsi.Name, &pCNSIType, &pURL, &cnsi.AuthorizationEndpoint, &cnsi.TokenEndpoint, &cnsi.DopplerLoggingEndpoint, &cnsi.SkipSSLValidation, &cnsi.ClientId, &cipherTextClientSecret, &cnsi.SSOAllowed, &subType, &metadata) + err := rows.Scan(&cnsi.GUID, &cnsi.Name, &pCNSIType, &pURL, &cnsi.AuthorizationEndpoint, &cnsi.TokenEndpoint, &cnsi.DopplerLoggingEndpoint, &cnsi.SkipSSLValidation, &cnsi.ClientId, &cipherTextClientSecret, &cnsi.SSOAllowed, &subType, &metadata, &cnsi.CreatedBy) if err != nil { return nil, fmt.Errorf("Unable to scan CNSI records: %v", err) } @@ -126,6 +127,9 @@ func (p *PostgresCNSIRepository) List(encryptionKey []byte) ([]*interfaces.CNSIR // rows.Close() + marshalJSON, _ := json.Marshal(cnsiList) + fmt.Println(string(marshalJSON)) + return cnsiList, nil } @@ -152,7 +156,7 @@ func (p *PostgresCNSIRepository) ListByUser(userGUID string) ([]*interfaces.Conn cluster := new(interfaces.ConnectedEndpoint) err := rows.Scan(&cluster.GUID, &cluster.Name, &pCNSIType, &pURL, &cluster.DopplerLoggingEndpoint, &cluster.Account, &cluster.TokenExpiry, &cluster.SkipSSLValidation, - &disconnected, &cluster.TokenMetadata, &subType, &metadata) + &disconnected, &cluster.TokenMetadata, &subType, &metadata, &cluster.CreatedBy) if err != nil { return nil, fmt.Errorf("Unable to scan cluster records: %v", err) } @@ -208,7 +212,7 @@ func (p *PostgresCNSIRepository) findBy(query, match string, encryptionKey []byt cnsi := new(interfaces.CNSIRecord) err := p.db.QueryRow(query, match).Scan(&cnsi.GUID, &cnsi.Name, &pCNSIType, &pURL, - &cnsi.AuthorizationEndpoint, &cnsi.TokenEndpoint, &cnsi.DopplerLoggingEndpoint, &cnsi.SkipSSLValidation, &cnsi.ClientId, &cipherTextClientSecret, &cnsi.SSOAllowed, &subType, &metadata) + &cnsi.AuthorizationEndpoint, &cnsi.TokenEndpoint, &cnsi.DopplerLoggingEndpoint, &cnsi.SkipSSLValidation, &cnsi.ClientId, &cipherTextClientSecret, &cnsi.SSOAllowed, &subType, &metadata, &cnsi.CreatedBy) switch { case err == sql.ErrNoRows: @@ -256,7 +260,7 @@ func (p *PostgresCNSIRepository) Save(guid string, cnsi interfaces.CNSIRecord, e } if _, err := p.db.Exec(saveCNSI, guid, cnsi.Name, fmt.Sprintf("%s", cnsi.CNSIType), fmt.Sprintf("%s", cnsi.APIEndpoint), cnsi.AuthorizationEndpoint, cnsi.TokenEndpoint, cnsi.DopplerLoggingEndpoint, cnsi.SkipSSLValidation, - cnsi.ClientId, cipherTextClientSecret, cnsi.SSOAllowed, cnsi.SubType, cnsi.Metadata); err != nil { + cnsi.ClientId, cipherTextClientSecret, cnsi.SSOAllowed, cnsi.SubType, cnsi.Metadata, cnsi.CreatedBy); err != nil { return fmt.Errorf("Unable to Save CNSI record: %v", err) } diff --git a/src/jetstream/repository/interfaces/portal_proxy.go b/src/jetstream/repository/interfaces/portal_proxy.go index e18731d5fc..1f7b396368 100644 --- a/src/jetstream/repository/interfaces/portal_proxy.go +++ b/src/jetstream/repository/interfaces/portal_proxy.go @@ -14,7 +14,7 @@ type PortalProxy interface { GetHttpClient(skipSSLValidation bool) http.Client GetHttpClientForRequest(req *http.Request, skipSSLValidation bool) http.Client RegisterEndpoint(c echo.Context, fetchInfo InfoFunc) error - DoRegisterEndpoint(cnsiName string, apiEndpoint string, skipSSLValidation bool, clientId string, clientSecret string, ssoAllowed bool, subType string, fetchInfo InfoFunc) (CNSIRecord, error) + DoRegisterEndpoint(cnsiName string, apiEndpoint string, skipSSLValidation bool, clientId string, clientSecret string, userId string, ssoAllowed bool, subType string, fetchInfo InfoFunc) (CNSIRecord, error) GetEndpointTypeSpec(typeName string) (EndpointPlugin, error) // Auth diff --git a/src/jetstream/repository/interfaces/structs.go b/src/jetstream/repository/interfaces/structs.go index 9077fed0d3..7bcb92306b 100644 --- a/src/jetstream/repository/interfaces/structs.go +++ b/src/jetstream/repository/interfaces/structs.go @@ -55,6 +55,7 @@ type CNSIRecord struct { SubType string `json:"sub_type"` Metadata string `json:"metadata"` Local bool `json:"local"` + CreatedBy string `json:"created_by"` } // ConnectedEndpoint @@ -72,6 +73,7 @@ type ConnectedEndpoint struct { SubType string `json:"sub_type"` EndpointMetadata string `json:"metadata"` Local bool `json:"local"` + CreatedBy string `json:"created_by"` } const ( From 818db960debc2bcdfad4a4615a3098884ddd972e Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Tue, 19 Jan 2021 15:45:13 +0100 Subject: [PATCH 004/101] Add endpointAdminMiddleware for endpoint routes (#4753) Signed-off-by: Thomas Quandt --- src/jetstream/main.go | 2 +- src/jetstream/middleware.go | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/jetstream/main.go b/src/jetstream/main.go index d384b12e07..4c8855950e 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -1112,7 +1112,7 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, needSetupMiddleware bool) { // API endpoints with Swagger documentation and accessible with an API key that require admin permissions stableAdminAPIGroup := stableAPIGroup - stableAdminAPIGroup.Use(p.adminMiddleware) + stableAdminAPIGroup.Use(p.endpointAdminMiddleware) // route endpoint creation requests to respecive plugins stableAdminAPIGroup.POST("/endpoints", p.pluginRegisterRouter) diff --git a/src/jetstream/middleware.go b/src/jetstream/middleware.go index a2d7adce31..ef9b1709fa 100644 --- a/src/jetstream/middleware.go +++ b/src/jetstream/middleware.go @@ -252,6 +252,33 @@ func (p *portalProxy) adminMiddleware(h echo.HandlerFunc) echo.HandlerFunc { } } +func (p *portalProxy) endpointAdminMiddleware(h echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + //check if user has endpointadmin or admin role + userID, err := p.GetSessionValue(c, "user_id") + if err == nil { + // check their admin status in UAA + u, err := p.StratosAuthService.GetUser(userID.(string)) + if err != nil { + return c.NoContent(http.StatusUnauthorized) + } + + //TODO needs to be adjusted so that stratos.endpointadmin isnt hardcoded here? + //like this a.p.Config.ConsoleConfig.ConsoleAdminScope + stratosEndpointAdmin := strings.Contains(strings.Join(u.Scopes, ""), "stratos.endpointadmin") + if stratosEndpointAdmin == true { + return h(c) + } + + //if user has no endpointAdmin role, continue to check for admin role + if u.Admin == true { + return h(c) + } + } + return handleSessionError(p.Config, c, errors.New("Unauthorized"), false, "You must be a Stratos admin or endpoint-admin to access this API") + } +} + func errorLoggingMiddleware(h echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { log.Debug("errorLoggingMiddleware") From 4be10b64b43fc6252cc3e583d493fdbf0e2fb0a2 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Tue, 19 Jan 2021 15:55:42 +0100 Subject: [PATCH 005/101] Cleanup debug log for testing (#4753) Signed-off-by: Thomas Quandt --- src/jetstream/repository/cnsis/pgsql_cnsis.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/jetstream/repository/cnsis/pgsql_cnsis.go b/src/jetstream/repository/cnsis/pgsql_cnsis.go index 393559d147..32458bf0e9 100644 --- a/src/jetstream/repository/cnsis/pgsql_cnsis.go +++ b/src/jetstream/repository/cnsis/pgsql_cnsis.go @@ -2,7 +2,6 @@ package cnsis import ( "database/sql" - "encoding/json" "errors" "fmt" "net/url" @@ -127,9 +126,6 @@ func (p *PostgresCNSIRepository) List(encryptionKey []byte) ([]*interfaces.CNSIR // rows.Close() - marshalJSON, _ := json.Marshal(cnsiList) - fmt.Println(string(marshalJSON)) - return cnsiList, nil } From 52861887a8aa0f597811dd9fffbf3f66e41ee47b Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Tue, 19 Jan 2021 16:17:00 +0100 Subject: [PATCH 006/101] Filter endpoints for endpointadmins (#4753) * Add a query to get all uaa tokens * Add a query to get all cnsis from the user who created it * Filter endpoints for endpointadmins - All admin endpoints and user-specific endpoints will be returned Signed-off-by: Thomas Quandt --- src/jetstream/cnsi.go | 60 +++++++++++++++ src/jetstream/repository/cnsis/pgsql_cnsis.go | 70 +++++++++++++++++ src/jetstream/repository/interfaces/cnsis.go | 1 + src/jetstream/repository/interfaces/tokens.go | 1 + .../repository/tokens/pgsql_tokens.go | 75 +++++++++++++++++++ 5 files changed, 207 insertions(+) diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 6986833f5a..552de0014d 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -197,6 +197,18 @@ func (p *portalProxy) unregisterCluster(c echo.Context) error { func (p *portalProxy) buildCNSIList(c echo.Context) ([]*interfaces.CNSIRecord, error) { log.Debug("buildCNSIList") + + //check user role and filter endpoints, if user has special role + userID, err := p.GetSessionValue(c, "user_id") + u, err := p.StratosAuthService.GetUser(userID.(string)) + if err != nil { + return nil, err + } + stratosEndpointAdmin := strings.Contains(strings.Join(u.Scopes, ""), "stratos.endpointadmin") + if stratosEndpointAdmin == true { + return p.ListAdminEndpoints(userID.(string)) + } + return p.ListEndpoints() } @@ -218,6 +230,54 @@ func (p *portalProxy) ListEndpoints() ([]*interfaces.CNSIRecord, error) { return cnsiList, nil } +//return a CNSI list with endpoints created by the current endpointadmin and all admins +func (p *portalProxy) ListAdminEndpoints(userID string) ([]*interfaces.CNSIRecord, error) { + log.Debug("ListAdminEndpoints") + + var cnsiList []*interfaces.CNSIRecord + var adminList []string + var err error + + //look up all admin ids in tokens + tokenRepo, err := p.GetStoreFactory().TokenStore() + if err != nil { + return cnsiList, fmt.Errorf("listRegisteredTokens: %s", err) + } + tokenList, err := tokenRepo.ListAuthToken(p.Config.EncryptionKeyInBytes) + if err != nil { + return cnsiList, err + } + + //search for admins and add them to list + for _, token := range tokenList { + tokenInfo, err := p.GetUserTokenInfo(token.AuthToken) + if err != nil { + return cnsiList, err + } + stratosAdmin := strings.Contains(strings.Join(tokenInfo.Scope, ""), "stratos.admin") + if stratosAdmin == true { + adminList = append(adminList, tokenInfo.UserGUID) + } + } + adminList = append(adminList, userID) + + //get a cnsi list from every admin found and current endpointadmin + cnsiRepo, err := p.GetStoreFactory().EndpointStore() + if err != nil { + return cnsiList, fmt.Errorf("listRegisteredCNSIs: %s", err) + } + + for _, adminID := range adminList { + creatorList, err := cnsiRepo.ListByCreator(adminID, p.Config.EncryptionKeyInBytes) + if err != nil { + return creatorList, err + } + cnsiList = append(cnsiList, creatorList...) + } + + return cnsiList, nil +} + // listCNSIs godoc // @Summary List endpoints // @Description diff --git a/src/jetstream/repository/cnsis/pgsql_cnsis.go b/src/jetstream/repository/cnsis/pgsql_cnsis.go index 32458bf0e9..5793bdeaa8 100644 --- a/src/jetstream/repository/cnsis/pgsql_cnsis.go +++ b/src/jetstream/repository/cnsis/pgsql_cnsis.go @@ -19,6 +19,10 @@ var listCNSIsByUser = `SELECT c.guid, c.name, c.cnsi_type, c.api_endpoint, c.dop FROM cnsis c, tokens t WHERE c.guid = t.cnsi_guid AND t.token_type=$1 AND t.user_guid=$2 AND t.disconnected = '0'` +var listCNSIsByCreator = `SELECT guid, name, cnsi_type, api_endpoint, auth_endpoint, token_endpoint, doppler_logging_endpoint, skip_ssl_validation, client_id, client_secret, sso_allowed, sub_type, meta_data, created_by + FROM cnsis + WHERE created_by=$1` + var findCNSI = `SELECT guid, name, cnsi_type, api_endpoint, auth_endpoint, token_endpoint, doppler_logging_endpoint, skip_ssl_validation, client_id, client_secret, sso_allowed, sub_type, meta_data, created_by FROM cnsis WHERE guid=$1` @@ -55,6 +59,7 @@ func InitRepositoryProvider(databaseProvider string) { // Modify the database statements if needed, for the given database type listCNSIs = datastore.ModifySQLStatement(listCNSIs, databaseProvider) listCNSIsByUser = datastore.ModifySQLStatement(listCNSIsByUser, databaseProvider) + listCNSIsByCreator = datastore.ModifySQLStatement(listCNSIsByCreator, databaseProvider) findCNSI = datastore.ModifySQLStatement(findCNSI, databaseProvider) findCNSIByAPIEndpoint = datastore.ModifySQLStatement(findCNSIByAPIEndpoint, databaseProvider) saveCNSI = datastore.ModifySQLStatement(saveCNSI, databaseProvider) @@ -183,6 +188,71 @@ func (p *PostgresCNSIRepository) ListByUser(userGUID string) ([]*interfaces.Conn return clusterList, nil } +// ListByCreator - Returns a list of CNSIs created by a user +func (p *PostgresCNSIRepository) ListByCreator(userGUID string, encryptionKey []byte) ([]*interfaces.CNSIRecord, error) { + log.Debug("ListByCreator") + rows, err := p.db.Query(listCNSIsByCreator, userGUID) + if err != nil { + return nil, fmt.Errorf("Unable to retrieve CNSI records: %v", err) + } + defer rows.Close() + + var cnsiList []*interfaces.CNSIRecord + cnsiList = make([]*interfaces.CNSIRecord, 0) + + for rows.Next() { + var ( + pCNSIType string + pURL string + cipherTextClientSecret []byte + subType sql.NullString + metadata sql.NullString + ) + + cnsi := new(interfaces.CNSIRecord) + + err := rows.Scan(&cnsi.GUID, &cnsi.Name, &pCNSIType, &pURL, &cnsi.AuthorizationEndpoint, &cnsi.TokenEndpoint, &cnsi.DopplerLoggingEndpoint, &cnsi.SkipSSLValidation, &cnsi.ClientId, &cipherTextClientSecret, &cnsi.SSOAllowed, &subType, &metadata, &cnsi.CreatedBy) + if err != nil { + return nil, fmt.Errorf("Unable to scan CNSI records: %v", err) + } + + cnsi.CNSIType = pCNSIType + + if cnsi.APIEndpoint, err = url.Parse(pURL); err != nil { + return nil, fmt.Errorf("Unable to parse API Endpoint: %v", err) + } + + if subType.Valid { + cnsi.SubType = subType.String + } + + if metadata.Valid { + cnsi.Metadata = metadata.String + } + + if len(cipherTextClientSecret) > 0 { + plaintextClientSecret, err := crypto.DecryptToken(encryptionKey, cipherTextClientSecret) + if err != nil { + return nil, err + } + cnsi.ClientSecret = plaintextClientSecret + } else { + // Empty secret means there was none, so set the plain text to an empty string + cnsi.ClientSecret = "" + } + + cnsiList = append(cnsiList, cnsi) + } + + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("Unable to List CNSI records: %v", err) + } + + // rows.Close() + + return cnsiList, nil +} + // Find - Returns a single CNSI Record func (p *PostgresCNSIRepository) Find(guid string, encryptionKey []byte) (interfaces.CNSIRecord, error) { log.Debug("Find") diff --git a/src/jetstream/repository/interfaces/cnsis.go b/src/jetstream/repository/interfaces/cnsis.go index 089b3cbb21..1144bb1932 100644 --- a/src/jetstream/repository/interfaces/cnsis.go +++ b/src/jetstream/repository/interfaces/cnsis.go @@ -4,6 +4,7 @@ package interfaces type EndpointRepository interface { List(encryptionKey []byte) ([]*CNSIRecord, error) ListByUser(userGUID string) ([]*ConnectedEndpoint, error) + ListByCreator(userGUID string, encryptionKey []byte) ([]*CNSIRecord, error) Find(guid string, encryptionKey []byte) (CNSIRecord, error) FindByAPIEndpoint(endpoint string, encryptionKey []byte) (CNSIRecord, error) Delete(guid string) error diff --git a/src/jetstream/repository/interfaces/tokens.go b/src/jetstream/repository/interfaces/tokens.go index 0467c8220b..b8b9f40a37 100644 --- a/src/jetstream/repository/interfaces/tokens.go +++ b/src/jetstream/repository/interfaces/tokens.go @@ -10,6 +10,7 @@ type Token struct { // TokenRepository is an application of the repository pattern for storing tokens type TokenRepository interface { FindAuthToken(userGUID string, encryptionKey []byte) (TokenRecord, error) + ListAuthToken(encryptionKey []byte) ([]TokenRecord, error) SaveAuthToken(userGUID string, tokenRecord TokenRecord, encryptionKey []byte) error FindCNSIToken(cnsiGUID string, userGUID string, encryptionKey []byte) (TokenRecord, error) diff --git a/src/jetstream/repository/tokens/pgsql_tokens.go b/src/jetstream/repository/tokens/pgsql_tokens.go index 89128b7839..f786e0e6df 100644 --- a/src/jetstream/repository/tokens/pgsql_tokens.go +++ b/src/jetstream/repository/tokens/pgsql_tokens.go @@ -16,6 +16,10 @@ var findAuthToken = `SELECT token_guid, auth_token, refresh_token, token_expiry, FROM tokens WHERE token_type = 'uaa' AND cnsi_guid = 'STRATOS' AND user_guid = $1` +var listAuthTokens = `SELECT token_guid, auth_token, refresh_token, token_expiry, auth_type, meta_data + FROM tokens + WHERE token_type = 'uaa' AND cnsi_guid = 'STRATOS'` + var countAuthTokens = `SELECT COUNT(*) FROM tokens WHERE token_type = 'uaa' AND cnsi_guid = 'STRATOS' AND user_guid = $1 ` @@ -84,6 +88,7 @@ func NewPgsqlTokenRepository(dcp *sql.DB) (interfaces.TokenRepository, error) { func InitRepositoryProvider(databaseProvider string) { // Modify the database statements if needed, for the given database type findAuthToken = datastore.ModifySQLStatement(findAuthToken, databaseProvider) + listAuthTokens = datastore.ModifySQLStatement(listAuthTokens, databaseProvider) countAuthTokens = datastore.ModifySQLStatement(countAuthTokens, databaseProvider) insertAuthToken = datastore.ModifySQLStatement(insertAuthToken, databaseProvider) updateAuthToken = datastore.ModifySQLStatement(updateAuthToken, databaseProvider) @@ -220,6 +225,76 @@ func (p *PgsqlTokenRepository) FindAuthToken(userGUID string, encryptionKey []by return *tr, nil } +// ListAuthToken - Return a list of Auth Tokens +func (p *PgsqlTokenRepository) ListAuthToken(encryptionKey []byte) ([]interfaces.TokenRecord, error) { + log.Debug("ListAuthTokens") + + var rows *sql.Rows + var err error + rows, err = p.db.Query(listAuthTokens) + if err != nil { + msg := "Unable to Find All CNSI tokens: %v" + if err == sql.ErrNoRows { + log.Debugf(msg, err) + } else { + log.Errorf(msg, err) + } + return make([]interfaces.TokenRecord, 0), fmt.Errorf(msg, err) + } + defer rows.Close() + + tokens := make([]interfaces.TokenRecord, 0) + for rows.Next() { + // temp vars to retrieve db data + var ( + tokenGUID sql.NullString + ciphertextAuthToken []byte + ciphertextRefreshToken []byte + tokenExpiry sql.NullInt64 + authType string + metadata sql.NullString + ) + + err := rows.Scan(&tokenGUID, &ciphertextAuthToken, &ciphertextRefreshToken, &tokenExpiry, &authType, &metadata) + if err != nil { + msg := "Unable to Find UAA token: %v" + log.Debugf(msg, err) + return make([]interfaces.TokenRecord, 0), fmt.Errorf(msg, err) + } + + log.Debug("Decrypting Auth Token") + plaintextAuthToken, err := crypto.DecryptToken(encryptionKey, ciphertextAuthToken) + if err != nil { + return make([]interfaces.TokenRecord, 0), err + } + + log.Debug("Decrypting Refresh Token") + plaintextRefreshToken, err := crypto.DecryptToken(encryptionKey, ciphertextRefreshToken) + if err != nil { + return make([]interfaces.TokenRecord, 0), err + } + + // Build a new TokenRecord based on the decrypted tokens + tr := new(interfaces.TokenRecord) + if tokenGUID.Valid { + tr.TokenGUID = tokenGUID.String + } + tr.AuthToken = plaintextAuthToken + tr.RefreshToken = plaintextRefreshToken + if tokenExpiry.Valid { + tr.TokenExpiry = tokenExpiry.Int64 + } + tr.AuthType = authType + if metadata.Valid { + tr.Metadata = metadata.String + } + + tokens = append(tokens, *tr) + } + + return tokens, nil +} + // SaveCNSIToken - Save the CNSI (UAA) token to the datastore func (p *PgsqlTokenRepository) SaveCNSIToken(cnsiGUID string, userGUID string, tr interfaces.TokenRecord, encryptionKey []byte) error { log.Debug("SaveCNSIToken") From 728bb4c93c4dd4e01cdf00707a4ff166370e943d Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Tue, 19 Jan 2021 16:33:24 +0100 Subject: [PATCH 007/101] Always filter endpoints unless user is an admin (#4753) Signed-off-by: Thomas Quandt --- src/jetstream/cnsi.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 552de0014d..4ae79f4021 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -198,18 +198,17 @@ func (p *portalProxy) unregisterCluster(c echo.Context) error { func (p *portalProxy) buildCNSIList(c echo.Context) ([]*interfaces.CNSIRecord, error) { log.Debug("buildCNSIList") - //check user role and filter endpoints, if user has special role userID, err := p.GetSessionValue(c, "user_id") u, err := p.StratosAuthService.GetUser(userID.(string)) if err != nil { return nil, err } - stratosEndpointAdmin := strings.Contains(strings.Join(u.Scopes, ""), "stratos.endpointadmin") - if stratosEndpointAdmin == true { - return p.ListAdminEndpoints(userID.(string)) + if u.Admin == true { + return p.ListEndpoints() } - return p.ListEndpoints() + //filter endpoints if user isn't admin + return p.ListAdminEndpoints(userID.(string)) } func (p *portalProxy) ListEndpoints() ([]*interfaces.CNSIRecord, error) { From e1c53fbdb750939d37b7091ffc4c087c6fae858f Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Wed, 20 Jan 2021 10:02:45 +0100 Subject: [PATCH 008/101] Change cnsiRecordExists to check for guid instead url (#4753) Signed-off-by: Thomas Quandt --- src/jetstream/cnsi.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 4ae79f4021..6d9ca36006 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -106,8 +106,12 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk "Failed to get API Endpoint: %v", err) } + h := sha1.New() + h.Write([]byte(apiEndpointURL.String() + userId)) + guid := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) + // check if we've already got this endpoint in the DB - ok := p.cnsiRecordExists(apiEndpoint) + ok := p.cnsiRecordExists(guid) if ok { // a record with the same api endpoint was found return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( @@ -133,10 +137,6 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk err) } - h := sha1.New() - h.Write([]byte(apiEndpointURL.String())) - guid := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) - newCNSI.Name = cnsiName newCNSI.APIEndpoint = apiEndpointURL newCNSI.SkipSSLValidation = skipSSLValidation @@ -260,7 +260,7 @@ func (p *portalProxy) ListAdminEndpoints(userID string) ([]*interfaces.CNSIRecor } adminList = append(adminList, userID) - //get a cnsi list from every admin found and current endpointadmin + //get a cnsi list from every admin found and given userID cnsiRepo, err := p.GetStoreFactory().EndpointStore() if err != nil { return cnsiList, fmt.Errorf("listRegisteredCNSIs: %s", err) @@ -430,10 +430,10 @@ func (p *portalProxy) GetCNSIRecordByEndpoint(endpoint string) (interfaces.CNSIR return rec, nil } -func (p *portalProxy) cnsiRecordExists(endpoint string) bool { +func (p *portalProxy) cnsiRecordExists(guid string) bool { log.Debug("cnsiRecordExists") - _, err := p.GetCNSIRecordByEndpoint(endpoint) + _, err := p.GetCNSIRecord(guid) return err == nil } From dbe2d39f45e64d8a75522fff78fa9f631fbee24a Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Mon, 25 Jan 2021 15:00:28 +0100 Subject: [PATCH 009/101] Split ENDPOINT_REGISTER and add CreatorInfo to endpoints (#4753) * Split ENDPOINT_REGISTER into two separate permissions to check for admin or endpointadmin * Add CreatorInfo to structs and EndpointDetail Signed-off-by: Thomas Quandt --- .../stratos-user-permissions.checker.ts | 11 ++++++--- .../endpoints-page.component.ts | 4 ++-- .../endpoint/base-endpoints-data-source.ts | 2 ++ .../endpoint-card.component.html | 8 +++++++ .../endpoint/endpoint-list.helpers.ts | 6 ++--- .../src/shared/user-permission.directive.ts | 24 +++++++++++++++---- .../store/src/types/endpoint.types.ts | 7 ++++++ src/jetstream/cnsi.go | 5 ---- src/jetstream/info.go | 10 ++++++++ .../repository/interfaces/structs.go | 6 +++++ 10 files changed, 65 insertions(+), 18 deletions(-) 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 294ba40f5b..d44a58635a 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 @@ -20,7 +20,8 @@ import { export enum StratosCurrentUserPermissions { - ENDPOINT_REGISTER = 'register.endpoint', + EDIT_ENDPOINT = 'edit-endpoint', + EDIT_ADMIN_ENDPOINT = 'edit-admin-endpoint', PASSWORD_CHANGE = 'change-password', EDIT_PROFILE = 'edit-profile', /** @@ -40,7 +41,7 @@ export enum StratosScopeStrings { SCIM_READ = 'scim.read', SCIM_WRITE = 'scim.write', STRATOS_NOAUTH = 'stratos.noauth', - STRATOS_ENDPOINTADMIN = 'stratos.user' //TODO needs to be implemented properly + STRATOS_ENDPOINTADMIN = 'stratos.endpointadmin' } export enum StratosPermissionTypes { @@ -53,10 +54,14 @@ export enum StratosPermissionTypes { // Every group result must be true in order for the permission to be true. A group result is true if all or some of it's permissions are // true (see `getCheckFromConfig`). export const stratosPermissionConfigs: IPermissionConfigs = { - [StratosCurrentUserPermissions.ENDPOINT_REGISTER]: new PermissionConfig( + [StratosCurrentUserPermissions.EDIT_ENDPOINT]: new PermissionConfig( StratosPermissionTypes.STRATOS_SCOPE, StratosScopeStrings.STRATOS_ENDPOINTADMIN ), + [StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT]: new PermissionConfig( + StratosPermissionTypes.STRATOS, + StratosPermissionStrings.STRATOS_ADMIN + ), [StratosCurrentUserPermissions.PASSWORD_CHANGE]: new PermissionConfig( StratosPermissionTypes.STRATOS_SCOPE, StratosScopeStrings.STRATOS_CHANGE_PASSWORD diff --git a/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts b/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts index 7904575755..5e3addf6b2 100644 --- a/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts @@ -45,7 +45,7 @@ import { SnackBarService } from '../../../shared/services/snackbar.service'; }, EndpointListHelper] }) export class EndpointsPageComponent implements AfterViewInit, OnDestroy, OnInit { - public canRegisterEndpoint = StratosCurrentUserPermissions.ENDPOINT_REGISTER; + public canRegisterEndpoint = [StratosCurrentUserPermissions.EDIT_ENDPOINT, StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT]; private healthCheckTimeout: number; public canBackupRestore$: Observable; @@ -91,7 +91,7 @@ export class EndpointsPageComponent implements AfterViewInit, OnDestroy, OnInit this.canBackupRestore$ = this.store.select(selectSessionData()).pipe( first(), map(sessionData => sessionData?.plugins.backup), - switchMap(enabled => enabled ? currentUserPermissionsService.can(this.canRegisterEndpoint) : of(false)) + switchMap(enabled => enabled ? currentUserPermissionsService.can(this.canRegisterEndpoint[0]) : of(false)) ); } diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts index 22f224cbe5..6d9283c617 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts @@ -123,6 +123,8 @@ export class BaseEndpointsDataSource extends ListDataSource { system_shared_token: false, metricsAvailable: false, sso_allowed: false, + created_by: "", + creator: {admin:false} }), paginationKey: action.paginationKey, isLocal: true, diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html index e5e6cb868e..279fa11629 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html @@ -44,6 +44,14 @@ + Details diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts index 37aff1a7a1..5fabff812e 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts @@ -108,7 +108,7 @@ export class EndpointListHelper { label: 'Disconnect', description: ``, // Description depends on console user permission createVisible: (row$: Observable) => combineLatest( - this.currentUserPermissionsService.can(StratosCurrentUserPermissions.ENDPOINT_REGISTER), + this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT), row$ ).pipe( map(([isAdmin, row]) => { @@ -155,7 +155,7 @@ export class EndpointListHelper { }, label: 'Unregister', description: 'Remove the endpoint', - createVisible: () => this.currentUserPermissionsService.can(StratosCurrentUserPermissions.ENDPOINT_REGISTER) + createVisible: () => this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT) }, { action: (item) => { @@ -164,7 +164,7 @@ export class EndpointListHelper { }, label: 'Edit endpoint', description: 'Edit the endpoint', - createVisible: () => this.currentUserPermissionsService.can(StratosCurrentUserPermissions.ENDPOINT_REGISTER) + createVisible: () => this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT) }, ...customActions ]; diff --git a/src/frontend/packages/core/src/shared/user-permission.directive.ts b/src/frontend/packages/core/src/shared/user-permission.directive.ts index 7b633408fb..29d0cb5d89 100644 --- a/src/frontend/packages/core/src/shared/user-permission.directive.ts +++ b/src/frontend/packages/core/src/shared/user-permission.directive.ts @@ -1,5 +1,6 @@ import { Directive, Input, OnDestroy, OnInit, TemplateRef, ViewContainerRef } from '@angular/core'; -import { Subscription } from 'rxjs'; +import { combineLatest, Observable, Subscription } from 'rxjs'; +import { map } from 'rxjs/operators'; import { PermissionTypes } from '../core/permissions/current-user-permissions.config'; import { CurrentUserPermissionsService } from '../core/permissions/current-user-permissions.service'; @@ -9,7 +10,7 @@ import { CurrentUserPermissionsService } from '../core/permissions/current-user- }) export class UserPermissionDirective implements OnDestroy, OnInit { @Input() - public appUserPermission: PermissionTypes; + public appUserPermission: PermissionTypes[]; @Input() public appUserPermissionEndpointGuid: string; @@ -23,9 +24,22 @@ export class UserPermissionDirective implements OnDestroy, OnInit { ) { } public ngOnInit() { - this.canSub = this.currentUserPermissionsService.can( - this.appUserPermission, - this.appUserPermissionEndpointGuid, + // execute a permission check for every give permissiontype + let $permissionChecks: Observable[]; + $permissionChecks = this.appUserPermission.map((permission: PermissionTypes) => { + return this.currentUserPermissionsService.can(permission,this.appUserPermissionEndpointGuid) + }); + + // permit user if one check results true + this.canSub = combineLatest($permissionChecks).pipe( + map((arr: boolean[])=>{ + for(const result of arr){ + if(result){ + return result; + } + } + return false; + }) ).subscribe(can => { if (can) { this.viewContainer.createEmbeddedView(this.templateRef); diff --git a/src/frontend/packages/store/src/types/endpoint.types.ts b/src/frontend/packages/store/src/types/endpoint.types.ts index 93cc4c831d..be5e499ddb 100644 --- a/src/frontend/packages/store/src/types/endpoint.types.ts +++ b/src/frontend/packages/store/src/types/endpoint.types.ts @@ -49,6 +49,8 @@ export interface EndpointModel { connectionStatus?: endpointConnectionStatus; metricsAvailable: boolean; local?: true; + created_by: string; + creator: CreatorInfo; } export const SystemSharedUserGuid = '00000000-1111-2222-3333-444444444444'; @@ -63,6 +65,11 @@ export interface EndpointUser { scopes?: UserScopeStrings[]; } +// Metadata for the user who created an endpoint +export interface CreatorInfo { + admin: boolean; +} + export interface EndpointState { loading: boolean; error: boolean; diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 6d9ca36006..a1648d589f 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -199,15 +199,10 @@ func (p *portalProxy) buildCNSIList(c echo.Context) ([]*interfaces.CNSIRecord, e log.Debug("buildCNSIList") userID, err := p.GetSessionValue(c, "user_id") - u, err := p.StratosAuthService.GetUser(userID.(string)) if err != nil { return nil, err } - if u.Admin == true { - return p.ListEndpoints() - } - //filter endpoints if user isn't admin return p.ListAdminEndpoints(userID.(string)) } diff --git a/src/jetstream/info.go b/src/jetstream/info.go index aaa8f1f204..f57c388a89 100644 --- a/src/jetstream/info.go +++ b/src/jetstream/info.go @@ -98,6 +98,16 @@ func (p *portalProxy) getInfo(c echo.Context) (*interfaces.Info, error) { endpoint.TokenMetadata = token.Metadata endpoint.SystemSharedToken = token.SystemShared } + + // try to get additional creator information for this cnsi + u, err := p.StratosAuthService.GetUser(cnsi.CreatedBy) + if err == nil { + creator := &interfaces.CreatorInfo{ + Admin: u.Admin, + } + endpoint.Creator = creator + } + cnsiType := cnsi.CNSIType _, ok = s.Endpoints[cnsiType] diff --git a/src/jetstream/repository/interfaces/structs.go b/src/jetstream/repository/interfaces/structs.go index 7bcb92306b..b87bb2e1cc 100644 --- a/src/jetstream/repository/interfaces/structs.go +++ b/src/jetstream/repository/interfaces/structs.go @@ -193,6 +193,11 @@ type ConnectedUser struct { Scopes []string `json:"scopes"` } +// CreatorInfo - additional information about the user who created an endpoint +type CreatorInfo struct { + Admin bool `json:"admin"` +} + type JWTUserTokenInfo struct { UserGUID string `json:"user_id"` UserName string `json:"user_name"` @@ -243,6 +248,7 @@ type EndpointDetail struct { *CNSIRecord EndpointMetadata interface{} `json:"endpoint_metadata,omitempty"` User *ConnectedUser `json:"user"` + Creator *CreatorInfo `json:"creator"` Metadata map[string]string `json:"metadata,omitempty"` TokenMetadata string `json:"-"` SystemSharedToken bool `json:"system_shared_token"` From cbace55a747f5b12eaa8089992aeb1d598613346 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Mon, 25 Jan 2021 16:58:12 +0100 Subject: [PATCH 010/101] Change permissions on endpoint-list menues (#4753) Signed-off-by: Thomas Quandt --- .../endpoints-page.component.ts | 2 +- .../endpoint-card.component.html | 4 +- .../endpoint/endpoint-list.helpers.ts | 37 ++++++++++++++++++- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts b/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts index 5e3addf6b2..ac54b4de52 100644 --- a/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts @@ -45,7 +45,7 @@ import { SnackBarService } from '../../../shared/services/snackbar.service'; }, EndpointListHelper] }) export class EndpointsPageComponent implements AfterViewInit, OnDestroy, OnInit { - public canRegisterEndpoint = [StratosCurrentUserPermissions.EDIT_ENDPOINT, StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT]; + public canRegisterEndpoint = [StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT, StratosCurrentUserPermissions.EDIT_ENDPOINT]; private healthCheckTimeout: number; public canBackupRestore$: Observable; diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html index 279fa11629..088da94032 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html @@ -44,14 +44,12 @@ - Details diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts index 5fabff812e..22fc0b07b9 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts @@ -1,3 +1,4 @@ +import { isNgTemplate } from '@angular/compiler'; import { ComponentFactoryResolver, ComponentRef, Injectable, ViewContainerRef } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; @@ -155,7 +156,23 @@ export class EndpointListHelper { }, label: 'Unregister', description: 'Remove the endpoint', - createVisible: () => this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT) + createVisible: (row$: Observable) => { + // TODO lock this behind featureflag + return combineLatest([ + this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT), + this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT), + row$ + ]).pipe( + map(([isAdmin, isEndpointAdmin, row])=>{ + if(row.creator.admin){ + return isAdmin; + }else{ + return isEndpointAdmin || isAdmin; + } + }) + ); + //return this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT) + } }, { action: (item) => { @@ -164,7 +181,23 @@ export class EndpointListHelper { }, label: 'Edit endpoint', description: 'Edit the endpoint', - createVisible: () => this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT) + createVisible: (row$: Observable) => { + // TODO lock this behind featureflag + return combineLatest([ + this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT), + this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT), + row$ + ]).pipe( + map(([isAdmin, isEndpointAdmin, row])=>{ + if(row.creator.admin){ + return isAdmin; + }else{ + return isEndpointAdmin || isAdmin; + } + }) + ); + //return this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT) + } }, ...customActions ]; From b89170709c16b90dd0b8cec26f3526682ce1eb09 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Wed, 27 Jan 2021 13:06:11 +0100 Subject: [PATCH 011/101] Change endpoint middleware to check for different permissions (#4753) Signed-off-by: Thomas Quandt --- src/jetstream/main.go | 2 +- src/jetstream/middleware.go | 41 ++++++++++++++++++++++++------------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/jetstream/main.go b/src/jetstream/main.go index 4c8855950e..269a8243b2 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -1112,7 +1112,7 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, needSetupMiddleware bool) { // API endpoints with Swagger documentation and accessible with an API key that require admin permissions stableAdminAPIGroup := stableAPIGroup - stableAdminAPIGroup.Use(p.endpointAdminMiddleware) + stableAdminAPIGroup.Use(p.endpointMiddleware) // route endpoint creation requests to respecive plugins stableAdminAPIGroup.POST("/endpoints", p.pluginRegisterRouter) diff --git a/src/jetstream/middleware.go b/src/jetstream/middleware.go index ef9b1709fa..8c4d3741ad 100644 --- a/src/jetstream/middleware.go +++ b/src/jetstream/middleware.go @@ -252,30 +252,43 @@ func (p *portalProxy) adminMiddleware(h echo.HandlerFunc) echo.HandlerFunc { } } -func (p *portalProxy) endpointAdminMiddleware(h echo.HandlerFunc) echo.HandlerFunc { +// endpointMiddleware - checks if user is admin or endpointadmin and has the necessary rights to edit the endpoint +func (p *portalProxy) endpointMiddleware(h echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { - //check if user has endpointadmin or admin role userID, err := p.GetSessionValue(c, "user_id") - if err == nil { - // check their admin status in UAA - u, err := p.StratosAuthService.GetUser(userID.(string)) + if err != nil { + return c.NoContent(http.StatusUnauthorized) + } + + u, err := p.StratosAuthService.GetUser(userID.(string)) + if err != nil { + return c.NoContent(http.StatusUnauthorized) + } + + // TODO remove hard coded value + endpointAdmin := strings.Contains(strings.Join(u.Scopes, ""), "stratos.endpointadmin") + if endpointAdmin == false && u.Admin == false { + return handleSessionError(p.Config, c, errors.New("Unauthorized"), false, "You must be a Stratos admin or endpoint-admin to access this API") + } + + endpointID := c.Param("id") + if len(endpointID) != 0 { + cnsiRecord, err := p.GetCNSIRecord(endpointID) if err != nil { return c.NoContent(http.StatusUnauthorized) } - //TODO needs to be adjusted so that stratos.endpointadmin isnt hardcoded here? - //like this a.p.Config.ConsoleConfig.ConsoleAdminScope - stratosEndpointAdmin := strings.Contains(strings.Join(u.Scopes, ""), "stratos.endpointadmin") - if stratosEndpointAdmin == true { - return h(c) + creator, err := p.StratosAuthService.GetUser(cnsiRecord.CreatedBy) + if err != nil { + return c.NoContent(http.StatusUnauthorized) } - //if user has no endpointAdmin role, continue to check for admin role - if u.Admin == true { - return h(c) + if creator.Admin == true && u.Admin == false { + return handleSessionError(p.Config, c, errors.New("Unauthorized"), false, "You must be Stratos admin to modify this endpoint.") } } - return handleSessionError(p.Config, c, errors.New("Unauthorized"), false, "You must be a Stratos admin or endpoint-admin to access this API") + + return h(c) } } From 6264d08f120d014968af1efbc4598c18e5d00fd3 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Wed, 27 Jan 2021 13:25:14 +0100 Subject: [PATCH 012/101] Add another permission check to endpointMiddleware (#4753) Signed-off-by: Thomas Quandt --- src/jetstream/middleware.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/jetstream/middleware.go b/src/jetstream/middleware.go index 8c4d3741ad..47462feb71 100644 --- a/src/jetstream/middleware.go +++ b/src/jetstream/middleware.go @@ -286,6 +286,10 @@ func (p *portalProxy) endpointMiddleware(h echo.HandlerFunc) echo.HandlerFunc { if creator.Admin == true && u.Admin == false { return handleSessionError(p.Config, c, errors.New("Unauthorized"), false, "You must be Stratos admin to modify this endpoint.") } + + if creator.Admin == false && u.Admin == false && creator.GUID != userID.(string) { + return handleSessionError(p.Config, c, errors.New("Unauthorized"), false, "Endpoint-admins are not allowed to modify endpoints created by other endpoint-admins.") + } } return h(c) From 4919e9fd1b90a5c201d4ebde643c9e03a76b1dc6 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Wed, 27 Jan 2021 15:39:42 +0100 Subject: [PATCH 013/101] Add env var to config and use it in the back-end (#4753) Signed-off-by: Thomas Quandt --- src/jetstream/cnsi.go | 19 +++++++++++++------ src/jetstream/config.dev | 3 +++ src/jetstream/main.go | 11 ++++++++++- src/jetstream/middleware.go | 1 + .../repository/interfaces/structs.go | 1 + 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index a1648d589f..d0c7af4a6f 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -107,7 +107,11 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk } h := sha1.New() - h.Write([]byte(apiEndpointURL.String() + userId)) + if p.GetConfig().EnableUserEndpoints == true { + h.Write([]byte(apiEndpointURL.String() + userId)) + } else { + h.Write([]byte(apiEndpointURL.String())) + } guid := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) // check if we've already got this endpoint in the DB @@ -198,12 +202,15 @@ func (p *portalProxy) unregisterCluster(c echo.Context) error { func (p *portalProxy) buildCNSIList(c echo.Context) ([]*interfaces.CNSIRecord, error) { log.Debug("buildCNSIList") - userID, err := p.GetSessionValue(c, "user_id") - if err != nil { - return nil, err + if p.GetConfig().EnableUserEndpoints == true { + userID, err := p.GetSessionValue(c, "user_id") + if err != nil { + return nil, err + } + return p.ListAdminEndpoints(userID.(string)) + } else { + return p.ListEndpoints() } - - return p.ListAdminEndpoints(userID.(string)) } func (p *portalProxy) ListEndpoints() ([]*interfaces.CNSIRecord, error) { diff --git a/src/jetstream/config.dev b/src/jetstream/config.dev index 43e6816d0c..e5a9a90380 100644 --- a/src/jetstream/config.dev +++ b/src/jetstream/config.dev @@ -57,6 +57,9 @@ LOCAL_USER=admin LOCAL_USER_PASSWORD=admin LOCAL_USER_SCOPE=stratos.admin +# Enable users with a stratos.endpointadmin role to create user endpoints +ENABLE_USER_ENDPOINTS=false + # Enable/disable API key-based access to Stratos API (disabled, admin_only, all_users). Default is admin_only #API_KEYS_ENABLED=admin_only diff --git a/src/jetstream/main.go b/src/jetstream/main.go index 269a8243b2..4f6956cbf1 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -298,6 +298,11 @@ func main() { log.Info("Initialization complete.") + //TODO delete later + enableuserendpoints := portalProxy.GetConfig().EnableUserEndpoints + fmt.Println("User Endpoints enabled?") + fmt.Println(enableuserendpoints) + c := make(chan os.Signal, 2) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { @@ -1112,7 +1117,11 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, needSetupMiddleware bool) { // API endpoints with Swagger documentation and accessible with an API key that require admin permissions stableAdminAPIGroup := stableAPIGroup - stableAdminAPIGroup.Use(p.endpointMiddleware) + if p.GetConfig().EnableUserEndpoints == true { + stableAdminAPIGroup.Use(p.endpointMiddleware) + } else { + stableAdminAPIGroup.Use(p.adminMiddleware) + } // route endpoint creation requests to respecive plugins stableAdminAPIGroup.POST("/endpoints", p.pluginRegisterRouter) diff --git a/src/jetstream/middleware.go b/src/jetstream/middleware.go index 47462feb71..fa062e3d31 100644 --- a/src/jetstream/middleware.go +++ b/src/jetstream/middleware.go @@ -271,6 +271,7 @@ func (p *portalProxy) endpointMiddleware(h echo.HandlerFunc) echo.HandlerFunc { return handleSessionError(p.Config, c, errors.New("Unauthorized"), false, "You must be a Stratos admin or endpoint-admin to access this API") } + // if id exists, then it's not a CREATE request endpointID := c.Param("id") if len(endpointID) != 0 { cnsiRecord, err := p.GetCNSIRecord(endpointID) diff --git a/src/jetstream/repository/interfaces/structs.go b/src/jetstream/repository/interfaces/structs.go index b87bb2e1cc..1b65f639aa 100644 --- a/src/jetstream/repository/interfaces/structs.go +++ b/src/jetstream/repository/interfaces/structs.go @@ -402,6 +402,7 @@ type PortalConfig struct { CanMigrateDatabaseSchema bool APIKeysEnabled config.APIKeysConfigValue `configName:"API_KEYS_ENABLED"` HomeViewShowFavoritesOnly bool `configName:"HOME_VIEW_SHOW_FAVORITES_ONLY"` + EnableUserEndpoints bool `configName:"ENABLE_USER_ENDPOINTS"` // 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 ee4e720bfe29978efa0dbb3b9f05e2b1ad451d36 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 28 Jan 2021 14:36:08 +0100 Subject: [PATCH 014/101] Implement env flag in front-end and consider empty creator as admin (#4753) * Add env var enableUserEndpoints to store model * Adjust components for enableUserEndpoints * Small adjustments in the back-end to enableUserEndpoints * Consider an empty string for the creator as an admin user Signed-off-by: Thomas Quandt --- .../endpoints-page.component.ts | 8 ++++++- .../endpoint-card.component.html | 2 +- .../endpoint-card/endpoint-card.component.ts | 4 ++++ .../endpoint/endpoint-list.helpers.ts | 24 +++++++++---------- .../src/shared/services/session.service.ts | 7 ++++++ .../packages/store/src/types/auth.types.ts | 1 + src/jetstream/cnsi.go | 8 +++++-- src/jetstream/info.go | 15 ++++++++---- src/jetstream/main.go | 5 ---- src/jetstream/middleware.go | 14 +++++++---- .../repository/interfaces/structs.go | 1 + 11 files changed, 59 insertions(+), 30 deletions(-) diff --git a/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts b/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts index ac54b4de52..29d79a534c 100644 --- a/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts @@ -34,6 +34,7 @@ import { } from '../../../shared/components/list/list-types/endpoint/endpoints-list-config.service'; import { ListConfig } from '../../../shared/components/list/list.component.types'; import { SnackBarService } from '../../../shared/services/snackbar.service'; +import { SessionService } from '../../../shared/services/session.service'; @Component({ selector: 'app-endpoints-page', @@ -45,7 +46,7 @@ import { SnackBarService } from '../../../shared/services/snackbar.service'; }, EndpointListHelper] }) export class EndpointsPageComponent implements AfterViewInit, OnDestroy, OnInit { - public canRegisterEndpoint = [StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT, StratosCurrentUserPermissions.EDIT_ENDPOINT]; + public canRegisterEndpoint = [StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT]; private healthCheckTimeout: number; public canBackupRestore$: Observable; @@ -68,6 +69,7 @@ export class EndpointsPageComponent implements AfterViewInit, OnDestroy, OnInit private snackBarService: SnackBarService, cs: CustomizationService, currentUserPermissionsService: CurrentUserPermissionsService, + public sessionService: SessionService ) { this.customizations = cs.get(); @@ -93,6 +95,10 @@ export class EndpointsPageComponent implements AfterViewInit, OnDestroy, OnInit map(sessionData => sessionData?.plugins.backup), switchMap(enabled => enabled ? currentUserPermissionsService.can(this.canRegisterEndpoint[0]) : of(false)) ); + + this.sessionService.userEndpointsEnabled().subscribe(enabled => { + if(enabled) this.canRegisterEndpoint.push(StratosCurrentUserPermissions.EDIT_ENDPOINT); + }); } subs: Subscription[] = []; diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html index 088da94032..a432179f91 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html @@ -44,7 +44,7 @@ - + Created by
{{ row.creator.admin ? "Admin" : "User" }}
diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.ts index c9df05430d..0451257ffa 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.ts @@ -32,6 +32,7 @@ import { BaseEndpointsDataSource } from '../base-endpoints-data-source'; import { EndpointListDetailsComponent, EndpointListHelper } from '../endpoint-list.helpers'; import { RouterNav } from './../../../../../../../../store/src/actions/router.actions'; import { CopyToClipboardComponent } from './../../../../copy-to-clipboard/copy-to-clipboard.component'; +import { SessionService } from '../../../../../services/session.service'; @Component({ selector: 'app-endpoint-card', @@ -54,6 +55,7 @@ export class EndpointCardComponent extends CardCell implements On public cardStatus$: Observable; private subs: Subscription[] = []; public connectionStatus: string; + public enableUserEndpoints$: Observable; private componentRef: ComponentRef; @@ -121,6 +123,7 @@ export class EndpointCardComponent extends CardCell implements On private endpointListHelper: EndpointListHelper, private componentFactoryResolver: ComponentFactoryResolver, private userFavoriteManager: UserFavoriteManager, + private sessionService: SessionService, ) { super(); this.endpointIds$ = this.endpointIds.asObservable(); @@ -130,6 +133,7 @@ export class EndpointCardComponent extends CardCell implements On this.favorite = this.userFavoriteManager.getFavoriteEndpointFromEntity(this.row); const e = this.endpointCatalogEntity.definition; this.hasDetails = !!e && !!e.listDetailsComponent; + this.enableUserEndpoints$ = this.sessionService.userEndpointsEnabled(); } ngOnDestroy(): void { diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts index 22fc0b07b9..de611bdde7 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts @@ -22,6 +22,7 @@ import { ConfirmationDialogService } from '../../../confirmation-dialog.service' import { createMetaCardMenuItemSeparator } from '../../list-cards/meta-card/meta-card-base/meta-card.component'; import { IListAction } from '../../list.component.types'; import { TableCellCustom } from '../../list.types'; +import { SessionService } from '../../../../../shared/services/session.service'; interface EndpointDetailsContainerRefs { componentRef: ComponentRef; @@ -67,6 +68,7 @@ export class EndpointListHelper { private currentUserPermissionsService: CurrentUserPermissionsService, private confirmDialog: ConfirmationDialogService, private snackBarService: SnackBarService, + private sessionService: SessionService, ) { } endpointActions(includeSeparators = false): IListAction[] { @@ -108,10 +110,10 @@ export class EndpointListHelper { }, label: 'Disconnect', description: ``, // Description depends on console user permission - createVisible: (row$: Observable) => combineLatest( - this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT), - row$ - ).pipe( + createVisible: (row$: Observable) => combineLatest([ + this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT), + row$ + ]).pipe( map(([isAdmin, row]) => { const isConnected = row.connectionStatus === 'connected'; return isConnected && (!row.system_shared_token || row.system_shared_token && isAdmin); @@ -157,21 +159,20 @@ export class EndpointListHelper { label: 'Unregister', description: 'Remove the endpoint', createVisible: (row$: Observable) => { - // TODO lock this behind featureflag return combineLatest([ + this.sessionService.userEndpointsEnabled(), this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT), this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT), row$ ]).pipe( - map(([isAdmin, isEndpointAdmin, row])=>{ - if(row.creator.admin){ + map(([userEndpointsEnabled, isAdmin, isEndpointAdmin, row])=>{ + if(!userEndpointsEnabled || row.creator.admin){ return isAdmin; }else{ return isEndpointAdmin || isAdmin; } }) ); - //return this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT) } }, { @@ -182,21 +183,20 @@ export class EndpointListHelper { label: 'Edit endpoint', description: 'Edit the endpoint', createVisible: (row$: Observable) => { - // TODO lock this behind featureflag return combineLatest([ + this.sessionService.userEndpointsEnabled(), this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT), this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT), row$ ]).pipe( - map(([isAdmin, isEndpointAdmin, row])=>{ - if(row.creator.admin){ + map(([userEndpointsEnabled, isAdmin, isEndpointAdmin, row])=>{ + if(!userEndpointsEnabled || row.creator.admin){ return isAdmin; }else{ return isEndpointAdmin || isAdmin; } }) ); - //return this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT) } }, ...customActions diff --git a/src/frontend/packages/core/src/shared/services/session.service.ts b/src/frontend/packages/core/src/shared/services/session.service.ts index 9990d0fed5..4253145203 100644 --- a/src/frontend/packages/core/src/shared/services/session.service.ts +++ b/src/frontend/packages/core/src/shared/services/session.service.ts @@ -17,4 +17,11 @@ export class SessionService { map(sessionData => sessionData.config.enableTechPreview || false) ); } + + userEndpointsEnabled(): Observable { + return this.store.select(selectSessionData()).pipe( + first(), + map(sessionData => sessionData.config.enableUserEndpoints || false) + ); + } } diff --git a/src/frontend/packages/store/src/types/auth.types.ts b/src/frontend/packages/store/src/types/auth.types.ts index e9730e6b3b..2f4a2f13db 100644 --- a/src/frontend/packages/store/src/types/auth.types.ts +++ b/src/frontend/packages/store/src/types/auth.types.ts @@ -34,6 +34,7 @@ export interface SessionDataConfig { APIKeysEnabled?: APIKeysEnabled; // Default value for Home View - show only favorited endpoints? homeViewShowFavoritesOnly?: boolean; + enableUserEndpoints?: boolean; } export interface SessionData { endpoints?: SessionEndpoints; diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index d0c7af4a6f..c328420b31 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -148,7 +148,10 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk newCNSI.ClientSecret = clientSecret newCNSI.SSOAllowed = ssoAllowed newCNSI.SubType = subType - newCNSI.CreatedBy = userId + + if p.GetConfig().EnableUserEndpoints == true { + newCNSI.CreatedBy = userId + } err = p.setCNSIRecord(guid, newCNSI) @@ -255,12 +258,13 @@ func (p *portalProxy) ListAdminEndpoints(userID string) ([]*interfaces.CNSIRecor if err != nil { return cnsiList, err } - stratosAdmin := strings.Contains(strings.Join(tokenInfo.Scope, ""), "stratos.admin") + stratosAdmin := strings.Contains(strings.Join(tokenInfo.Scope, ""), p.GetConfig().ConsoleConfig.ConsoleAdminScope) if stratosAdmin == true { adminList = append(adminList, tokenInfo.UserGUID) } } adminList = append(adminList, userID) + adminList = append(adminList, "") //legacy endpoints dont have a creator //get a cnsi list from every admin found and given userID cnsiRepo, err := p.GetStoreFactory().EndpointStore() diff --git a/src/jetstream/info.go b/src/jetstream/info.go index f57c388a89..69ad213339 100644 --- a/src/jetstream/info.go +++ b/src/jetstream/info.go @@ -62,6 +62,7 @@ func (p *portalProxy) getInfo(c echo.Context) (*interfaces.Info, error) { s.Configuration.ListAllowLoadMaxed = p.Config.UIListAllowLoadMaxed s.Configuration.APIKeysEnabled = string(p.Config.APIKeysEnabled) s.Configuration.HomeViewShowFavoritesOnly = p.Config.HomeViewShowFavoritesOnly + s.Configuration.EnableUserEndpoints = p.Config.EnableUserEndpoints // Only add diagnostics information if the user is an admin if uaaUser.Admin { @@ -99,13 +100,17 @@ func (p *portalProxy) getInfo(c echo.Context) (*interfaces.Info, error) { endpoint.SystemSharedToken = token.SystemShared } + // set the creator preemptively as admin, if no id is found + endpoint.Creator = &interfaces.CreatorInfo{ + Admin: true, + } + // try to get additional creator information for this cnsi - u, err := p.StratosAuthService.GetUser(cnsi.CreatedBy) - if err == nil { - creator := &interfaces.CreatorInfo{ - Admin: u.Admin, + if len(cnsi.CreatedBy) != 0 { + u, err := p.StratosAuthService.GetUser(cnsi.CreatedBy) + if err == nil { + endpoint.Creator.Admin = u.Admin } - endpoint.Creator = creator } cnsiType := cnsi.CNSIType diff --git a/src/jetstream/main.go b/src/jetstream/main.go index 4f6956cbf1..60f7f766cc 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -298,11 +298,6 @@ func main() { log.Info("Initialization complete.") - //TODO delete later - enableuserendpoints := portalProxy.GetConfig().EnableUserEndpoints - fmt.Println("User Endpoints enabled?") - fmt.Println(enableuserendpoints) - c := make(chan os.Signal, 2) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { diff --git a/src/jetstream/middleware.go b/src/jetstream/middleware.go index fa062e3d31..f9b38e85ea 100644 --- a/src/jetstream/middleware.go +++ b/src/jetstream/middleware.go @@ -279,16 +279,22 @@ func (p *portalProxy) endpointMiddleware(h echo.HandlerFunc) echo.HandlerFunc { return c.NoContent(http.StatusUnauthorized) } - creator, err := p.StratosAuthService.GetUser(cnsiRecord.CreatedBy) - if err != nil { - return c.NoContent(http.StatusUnauthorized) + // if CreatedBy is not set, then its a legacy admin-endpoint + creator := &interfaces.ConnectedUser{} + creator.Admin = true + if len(cnsiRecord.CreatedBy) != 0 { + creatorRecord, err := p.StratosAuthService.GetUser(cnsiRecord.CreatedBy) + if err != nil { + return c.NoContent(http.StatusUnauthorized) + } + creator = creatorRecord } if creator.Admin == true && u.Admin == false { return handleSessionError(p.Config, c, errors.New("Unauthorized"), false, "You must be Stratos admin to modify this endpoint.") } - if creator.Admin == false && u.Admin == false && creator.GUID != userID.(string) { + if creator.Admin == false && u.Admin == false && len(creator.GUID) != 0 && creator.GUID != userID.(string) { return handleSessionError(p.Config, c, errors.New("Unauthorized"), false, "Endpoint-admins are not allowed to modify endpoints created by other endpoint-admins.") } } diff --git a/src/jetstream/repository/interfaces/structs.go b/src/jetstream/repository/interfaces/structs.go index 1b65f639aa..e1887f7dd9 100644 --- a/src/jetstream/repository/interfaces/structs.go +++ b/src/jetstream/repository/interfaces/structs.go @@ -240,6 +240,7 @@ type Info struct { ListAllowLoadMaxed bool `json:"listAllowLoadMaxed,omitempty"` APIKeysEnabled string `json:"APIKeysEnabled"` HomeViewShowFavoritesOnly bool `json:"homeViewShowFavoritesOnly"` + EnableUserEndpoints bool `json:"enableUserEndpoints"` } `json:"config"` } From 098d61ce3eaced267b2d366cfb05f70fdf7ec2bc Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 28 Jan 2021 16:43:03 +0100 Subject: [PATCH 015/101] Clean up a few lines (#4753) Signed-off-by: Thomas Quandt --- src/jetstream/middleware.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/jetstream/middleware.go b/src/jetstream/middleware.go index f9b38e85ea..fd59b01741 100644 --- a/src/jetstream/middleware.go +++ b/src/jetstream/middleware.go @@ -280,8 +280,9 @@ func (p *portalProxy) endpointMiddleware(h echo.HandlerFunc) echo.HandlerFunc { } // if CreatedBy is not set, then its a legacy admin-endpoint - creator := &interfaces.ConnectedUser{} - creator.Admin = true + creator := &interfaces.ConnectedUser{ + Admin: true, + } if len(cnsiRecord.CreatedBy) != 0 { creatorRecord, err := p.StratosAuthService.GetUser(cnsiRecord.CreatedBy) if err != nil { From ec6cb1a322eec931ec8e7d7e4492385404b69238 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Fri, 29 Jan 2021 15:55:05 +0100 Subject: [PATCH 016/101] Admin can save over user endpoints (#4753) * User endpoints with same APIEndpoint will be automatically removed when admin creates endpoint * Split list functions in pgsql_cnsi into a generic function Signed-off-by: Thomas Quandt --- src/jetstream/cnsi.go | 74 ++++++++++++++++++- src/jetstream/repository/cnsis/pgsql_cnsis.go | 15 +++- src/jetstream/repository/interfaces/cnsis.go | 1 + 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index c328420b31..7ea22a5441 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -69,7 +69,6 @@ func (p *portalProxy) RegisterEndpoint(c echo.Context, fetchInfo interfaces.Info sessionValue, err := p.GetSessionValue(c, "user_id") if err != nil { - //maybe better to just return error? return interfaces.NewHTTPShadowError( http.StatusInternalServerError, "Failed to get session user", @@ -125,6 +124,47 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk ) } + if p.GetConfig().EnableUserEndpoints == true { + // is the current user an admin -> delete user endpoints? + currentCreator, err := p.StratosAuthService.GetUser(userId) + if err != nil { + return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( + http.StatusInternalServerError, + "Failed to get user information", + "Failed to get user information: %v", err) + } + + // get all endpoints determined by the APIEndpoint + duplicateEndpoints, err := p.listCNSIByAPIEndpoint(apiEndpoint) + if err != nil { + return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( + http.StatusBadRequest, + "Failed to check other endpoints", + "Failed to check other endpoints: %v", + err) + } + + // check if we've already got this APIEndpoint from a admin in this DB + for _, duplicate := range duplicateEndpoints { + // filter out all endpoints created by users + creatorRecord, err := p.StratosAuthService.GetUser(duplicate.CreatedBy) + if len(duplicate.CreatedBy) != 0 || creatorRecord.Admin == true || err != nil { + return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( + http.StatusBadRequest, + "Can not register same admin endpoint multiple times", + "Can not register same admin endpoint multiple times", + ) + } + } + + if currentCreator.Admin == true { + // remove all endpoints with same APIEndpoint + for _, duplicate := range duplicateEndpoints { + p.doUnregisterCluster(duplicate.GUID) + } + } + } + newCNSI, _, err := fetchInfo(apiEndpoint, skipSSLValidation) if err != nil { if ok, detail := isSSLRelatedError(err); ok { @@ -148,7 +188,6 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk newCNSI.ClientSecret = clientSecret newCNSI.SSOAllowed = ssoAllowed newCNSI.SubType = subType - if p.GetConfig().EnableUserEndpoints == true { newCNSI.CreatedBy = userId } @@ -191,6 +230,12 @@ func (p *portalProxy) unregisterCluster(c echo.Context) error { "Missing target endpoint", "Need CNSI GUID passed as form param") } + + return p.doUnregisterCluster(cnsiGUID) +} + +func (p *portalProxy) doUnregisterCluster(cnsiGUID string) error { + // Should check for errors? p.unsetCNSIRecord(cnsiGUID) @@ -279,6 +324,31 @@ func (p *portalProxy) ListAdminEndpoints(userID string) ([]*interfaces.CNSIRecor } cnsiList = append(cnsiList, creatorList...) } + return cnsiList, nil +} + +// listCNSIByAPIEndpoint - receives a URL as string (must be formatted like it's saved) +func (p *portalProxy) listCNSIByAPIEndpoint(apiEndpoint string) ([]*interfaces.CNSIRecord, error) { + log.Debug("listCNSIByAPIEndpoint") + + var cnsiList []*interfaces.CNSIRecord + var err error + + // Remove trailing slash, if there is one + apiEndpointURL, err := url.Parse(apiEndpoint) + if err != nil { + return nil, fmt.Errorf("Failed to get API Endpoint: %s", err) + } + + cnsiRepo, err := p.GetStoreFactory().EndpointStore() + if err != nil { + return cnsiList, fmt.Errorf("listCNSIByAPIEndpoint: %s", err) + } + + cnsiList, err = cnsiRepo.ListByAPIEndpoint(fmt.Sprintf("%s", apiEndpointURL), p.Config.EncryptionKeyInBytes) + if err != nil { + return cnsiList, err + } return cnsiList, nil } diff --git a/src/jetstream/repository/cnsis/pgsql_cnsis.go b/src/jetstream/repository/cnsis/pgsql_cnsis.go index 5793bdeaa8..222a9c997f 100644 --- a/src/jetstream/repository/cnsis/pgsql_cnsis.go +++ b/src/jetstream/repository/cnsis/pgsql_cnsis.go @@ -191,7 +191,18 @@ func (p *PostgresCNSIRepository) ListByUser(userGUID string) ([]*interfaces.Conn // ListByCreator - Returns a list of CNSIs created by a user func (p *PostgresCNSIRepository) ListByCreator(userGUID string, encryptionKey []byte) ([]*interfaces.CNSIRecord, error) { log.Debug("ListByCreator") - rows, err := p.db.Query(listCNSIsByCreator, userGUID) + return p.listBy(listCNSIsByCreator, userGUID, encryptionKey) +} + +// ListByAPIEndpoint - Returns a a list of CNSIs with the same APIEndpoint +func (p *PostgresCNSIRepository) ListByAPIEndpoint(endpoint string, encryptionKey []byte) ([]*interfaces.CNSIRecord, error) { + log.Debug("listByAPIEndpoint") + return p.listBy(findCNSIByAPIEndpoint, endpoint, encryptionKey) +} + +// listBy - Returns a list of CNSI Records found using the given query looking for match +func (p *PostgresCNSIRepository) listBy(query string, match string, encryptionKey []byte) ([]*interfaces.CNSIRecord, error) { + rows, err := p.db.Query(query, match) if err != nil { return nil, fmt.Errorf("Unable to retrieve CNSI records: %v", err) } @@ -248,8 +259,6 @@ func (p *PostgresCNSIRepository) ListByCreator(userGUID string, encryptionKey [] return nil, fmt.Errorf("Unable to List CNSI records: %v", err) } - // rows.Close() - return cnsiList, nil } diff --git a/src/jetstream/repository/interfaces/cnsis.go b/src/jetstream/repository/interfaces/cnsis.go index 1144bb1932..d142e689d7 100644 --- a/src/jetstream/repository/interfaces/cnsis.go +++ b/src/jetstream/repository/interfaces/cnsis.go @@ -5,6 +5,7 @@ type EndpointRepository interface { List(encryptionKey []byte) ([]*CNSIRecord, error) ListByUser(userGUID string) ([]*ConnectedEndpoint, error) ListByCreator(userGUID string, encryptionKey []byte) ([]*CNSIRecord, error) + ListByAPIEndpoint(endpoint string, encryptionKey []byte) ([]*CNSIRecord, error) Find(guid string, encryptionKey []byte) (CNSIRecord, error) FindByAPIEndpoint(endpoint string, encryptionKey []byte) (CNSIRecord, error) Delete(guid string) error From 1d659a7b8915d8a46108d912550d501d5e0838ef Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Mon, 1 Feb 2021 11:25:49 +0100 Subject: [PATCH 017/101] Rename created_by into creator and the id from the front-end (#4753) Signed-off-by: Thomas Quandt --- .../endpoint/base-endpoints-data-source.ts | 1 - .../store/src/types/endpoint.types.ts | 1 - src/jetstream/cnsi.go | 6 ++--- ...CreatedBy.go => 20210201110000_Creator.go} | 4 +-- src/jetstream/info.go | 4 +-- src/jetstream/middleware.go | 6 ++--- src/jetstream/repository/cnsis/pgsql_cnsis.go | 26 +++++++++---------- .../repository/interfaces/structs.go | 4 +-- 8 files changed, 24 insertions(+), 28 deletions(-) rename src/jetstream/datastore/{20210119150000_CreatedBy.go => 20210201110000_Creator.go} (51%) diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts index 6d9283c617..0dbfe05076 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts @@ -123,7 +123,6 @@ export class BaseEndpointsDataSource extends ListDataSource { system_shared_token: false, metricsAvailable: false, sso_allowed: false, - created_by: "", creator: {admin:false} }), paginationKey: action.paginationKey, diff --git a/src/frontend/packages/store/src/types/endpoint.types.ts b/src/frontend/packages/store/src/types/endpoint.types.ts index be5e499ddb..9e09cc9a78 100644 --- a/src/frontend/packages/store/src/types/endpoint.types.ts +++ b/src/frontend/packages/store/src/types/endpoint.types.ts @@ -49,7 +49,6 @@ export interface EndpointModel { connectionStatus?: endpointConnectionStatus; metricsAvailable: boolean; local?: true; - created_by: string; creator: CreatorInfo; } diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 7ea22a5441..30a138c124 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -147,8 +147,8 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk // check if we've already got this APIEndpoint from a admin in this DB for _, duplicate := range duplicateEndpoints { // filter out all endpoints created by users - creatorRecord, err := p.StratosAuthService.GetUser(duplicate.CreatedBy) - if len(duplicate.CreatedBy) != 0 || creatorRecord.Admin == true || err != nil { + creatorRecord, err := p.StratosAuthService.GetUser(duplicate.Creator) + if len(duplicate.Creator) != 0 || creatorRecord.Admin == true || err != nil { return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( http.StatusBadRequest, "Can not register same admin endpoint multiple times", @@ -189,7 +189,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk newCNSI.SSOAllowed = ssoAllowed newCNSI.SubType = subType if p.GetConfig().EnableUserEndpoints == true { - newCNSI.CreatedBy = userId + newCNSI.Creator = userId } err = p.setCNSIRecord(guid, newCNSI) diff --git a/src/jetstream/datastore/20210119150000_CreatedBy.go b/src/jetstream/datastore/20210201110000_Creator.go similarity index 51% rename from src/jetstream/datastore/20210119150000_CreatedBy.go rename to src/jetstream/datastore/20210201110000_Creator.go index b3e711ad7c..f5dda15cc1 100644 --- a/src/jetstream/datastore/20210119150000_CreatedBy.go +++ b/src/jetstream/datastore/20210201110000_Creator.go @@ -7,8 +7,8 @@ import ( ) func init() { - RegisterMigration(20210119150000, "CreatedBy", func(txn *sql.Tx, conf *goose.DBConf) error { - alterCNSI := "ALTER TABLE cnsis ADD COLUMN created_by VARCHAR(36) NOT NULL DEFAULT '';" + RegisterMigration(20210201110000, "Creator", func(txn *sql.Tx, conf *goose.DBConf) error { + alterCNSI := "ALTER TABLE cnsis ADD COLUMN creator VARCHAR(36) NOT NULL DEFAULT '';" _, err := txn.Exec(alterCNSI) if err != nil { diff --git a/src/jetstream/info.go b/src/jetstream/info.go index 69ad213339..b3bac96749 100644 --- a/src/jetstream/info.go +++ b/src/jetstream/info.go @@ -106,8 +106,8 @@ func (p *portalProxy) getInfo(c echo.Context) (*interfaces.Info, error) { } // try to get additional creator information for this cnsi - if len(cnsi.CreatedBy) != 0 { - u, err := p.StratosAuthService.GetUser(cnsi.CreatedBy) + if len(cnsi.Creator) != 0 { + u, err := p.StratosAuthService.GetUser(cnsi.Creator) if err == nil { endpoint.Creator.Admin = u.Admin } diff --git a/src/jetstream/middleware.go b/src/jetstream/middleware.go index fd59b01741..1912de0045 100644 --- a/src/jetstream/middleware.go +++ b/src/jetstream/middleware.go @@ -279,12 +279,12 @@ func (p *portalProxy) endpointMiddleware(h echo.HandlerFunc) echo.HandlerFunc { return c.NoContent(http.StatusUnauthorized) } - // if CreatedBy is not set, then its a legacy admin-endpoint + // if Creator is not set, then its a legacy admin-endpoint creator := &interfaces.ConnectedUser{ Admin: true, } - if len(cnsiRecord.CreatedBy) != 0 { - creatorRecord, err := p.StratosAuthService.GetUser(cnsiRecord.CreatedBy) + if len(cnsiRecord.Creator) != 0 { + creatorRecord, err := p.StratosAuthService.GetUser(cnsiRecord.Creator) if err != nil { return c.NoContent(http.StatusUnauthorized) } diff --git a/src/jetstream/repository/cnsis/pgsql_cnsis.go b/src/jetstream/repository/cnsis/pgsql_cnsis.go index 222a9c997f..ae8a4a05b2 100644 --- a/src/jetstream/repository/cnsis/pgsql_cnsis.go +++ b/src/jetstream/repository/cnsis/pgsql_cnsis.go @@ -12,26 +12,26 @@ import ( log "github.com/sirupsen/logrus" ) -var listCNSIs = `SELECT guid, name, cnsi_type, api_endpoint, auth_endpoint, token_endpoint, doppler_logging_endpoint, skip_ssl_validation, client_id, client_secret, sso_allowed, sub_type, meta_data, created_by +var listCNSIs = `SELECT guid, name, cnsi_type, api_endpoint, auth_endpoint, token_endpoint, doppler_logging_endpoint, skip_ssl_validation, client_id, client_secret, sso_allowed, sub_type, meta_data, creator FROM cnsis` -var listCNSIsByUser = `SELECT c.guid, c.name, c.cnsi_type, c.api_endpoint, c.doppler_logging_endpoint, t.user_guid, t.token_expiry, c.skip_ssl_validation, t.disconnected, t.meta_data, c.sub_type, c.meta_data as endpoint_metadata, c.created_by +var listCNSIsByUser = `SELECT c.guid, c.name, c.cnsi_type, c.api_endpoint, c.doppler_logging_endpoint, t.user_guid, t.token_expiry, c.skip_ssl_validation, t.disconnected, t.meta_data, c.sub_type, c.meta_data as endpoint_metadata, c.creator FROM cnsis c, tokens t WHERE c.guid = t.cnsi_guid AND t.token_type=$1 AND t.user_guid=$2 AND t.disconnected = '0'` -var listCNSIsByCreator = `SELECT guid, name, cnsi_type, api_endpoint, auth_endpoint, token_endpoint, doppler_logging_endpoint, skip_ssl_validation, client_id, client_secret, sso_allowed, sub_type, meta_data, created_by +var listCNSIsByCreator = `SELECT guid, name, cnsi_type, api_endpoint, auth_endpoint, token_endpoint, doppler_logging_endpoint, skip_ssl_validation, client_id, client_secret, sso_allowed, sub_type, meta_data, creator FROM cnsis - WHERE created_by=$1` + WHERE creator=$1` -var findCNSI = `SELECT guid, name, cnsi_type, api_endpoint, auth_endpoint, token_endpoint, doppler_logging_endpoint, skip_ssl_validation, client_id, client_secret, sso_allowed, sub_type, meta_data, created_by +var findCNSI = `SELECT guid, name, cnsi_type, api_endpoint, auth_endpoint, token_endpoint, doppler_logging_endpoint, skip_ssl_validation, client_id, client_secret, sso_allowed, sub_type, meta_data, creator FROM cnsis WHERE guid=$1` -var findCNSIByAPIEndpoint = `SELECT guid, name, cnsi_type, api_endpoint, auth_endpoint, token_endpoint, doppler_logging_endpoint, skip_ssl_validation, client_id, client_secret, sso_allowed, sub_type, meta_data, created_by +var findCNSIByAPIEndpoint = `SELECT guid, name, cnsi_type, api_endpoint, auth_endpoint, token_endpoint, doppler_logging_endpoint, skip_ssl_validation, client_id, client_secret, sso_allowed, sub_type, meta_data, creator FROM cnsis WHERE api_endpoint=$1` -var saveCNSI = `INSERT INTO cnsis (guid, name, cnsi_type, api_endpoint, auth_endpoint, token_endpoint, doppler_logging_endpoint, skip_ssl_validation, client_id, client_secret, sso_allowed, sub_type, meta_data, created_by) +var saveCNSI = `INSERT INTO cnsis (guid, name, cnsi_type, api_endpoint, auth_endpoint, token_endpoint, doppler_logging_endpoint, skip_ssl_validation, client_id, client_secret, sso_allowed, sub_type, meta_data, creator) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)` var deleteCNSI = `DELETE FROM cnsis WHERE guid = $1` @@ -92,7 +92,7 @@ func (p *PostgresCNSIRepository) List(encryptionKey []byte) ([]*interfaces.CNSIR cnsi := new(interfaces.CNSIRecord) - err := rows.Scan(&cnsi.GUID, &cnsi.Name, &pCNSIType, &pURL, &cnsi.AuthorizationEndpoint, &cnsi.TokenEndpoint, &cnsi.DopplerLoggingEndpoint, &cnsi.SkipSSLValidation, &cnsi.ClientId, &cipherTextClientSecret, &cnsi.SSOAllowed, &subType, &metadata, &cnsi.CreatedBy) + err := rows.Scan(&cnsi.GUID, &cnsi.Name, &pCNSIType, &pURL, &cnsi.AuthorizationEndpoint, &cnsi.TokenEndpoint, &cnsi.DopplerLoggingEndpoint, &cnsi.SkipSSLValidation, &cnsi.ClientId, &cipherTextClientSecret, &cnsi.SSOAllowed, &subType, &metadata, &cnsi.Creator) if err != nil { return nil, fmt.Errorf("Unable to scan CNSI records: %v", err) } @@ -129,8 +129,6 @@ func (p *PostgresCNSIRepository) List(encryptionKey []byte) ([]*interfaces.CNSIR return nil, fmt.Errorf("Unable to List CNSI records: %v", err) } - // rows.Close() - return cnsiList, nil } @@ -157,7 +155,7 @@ func (p *PostgresCNSIRepository) ListByUser(userGUID string) ([]*interfaces.Conn cluster := new(interfaces.ConnectedEndpoint) err := rows.Scan(&cluster.GUID, &cluster.Name, &pCNSIType, &pURL, &cluster.DopplerLoggingEndpoint, &cluster.Account, &cluster.TokenExpiry, &cluster.SkipSSLValidation, - &disconnected, &cluster.TokenMetadata, &subType, &metadata, &cluster.CreatedBy) + &disconnected, &cluster.TokenMetadata, &subType, &metadata, &cluster.Creator) if err != nil { return nil, fmt.Errorf("Unable to scan cluster records: %v", err) } @@ -222,7 +220,7 @@ func (p *PostgresCNSIRepository) listBy(query string, match string, encryptionKe cnsi := new(interfaces.CNSIRecord) - err := rows.Scan(&cnsi.GUID, &cnsi.Name, &pCNSIType, &pURL, &cnsi.AuthorizationEndpoint, &cnsi.TokenEndpoint, &cnsi.DopplerLoggingEndpoint, &cnsi.SkipSSLValidation, &cnsi.ClientId, &cipherTextClientSecret, &cnsi.SSOAllowed, &subType, &metadata, &cnsi.CreatedBy) + err := rows.Scan(&cnsi.GUID, &cnsi.Name, &pCNSIType, &pURL, &cnsi.AuthorizationEndpoint, &cnsi.TokenEndpoint, &cnsi.DopplerLoggingEndpoint, &cnsi.SkipSSLValidation, &cnsi.ClientId, &cipherTextClientSecret, &cnsi.SSOAllowed, &subType, &metadata, &cnsi.Creator) if err != nil { return nil, fmt.Errorf("Unable to scan CNSI records: %v", err) } @@ -287,7 +285,7 @@ func (p *PostgresCNSIRepository) findBy(query, match string, encryptionKey []byt cnsi := new(interfaces.CNSIRecord) err := p.db.QueryRow(query, match).Scan(&cnsi.GUID, &cnsi.Name, &pCNSIType, &pURL, - &cnsi.AuthorizationEndpoint, &cnsi.TokenEndpoint, &cnsi.DopplerLoggingEndpoint, &cnsi.SkipSSLValidation, &cnsi.ClientId, &cipherTextClientSecret, &cnsi.SSOAllowed, &subType, &metadata, &cnsi.CreatedBy) + &cnsi.AuthorizationEndpoint, &cnsi.TokenEndpoint, &cnsi.DopplerLoggingEndpoint, &cnsi.SkipSSLValidation, &cnsi.ClientId, &cipherTextClientSecret, &cnsi.SSOAllowed, &subType, &metadata, &cnsi.Creator) switch { case err == sql.ErrNoRows: @@ -335,7 +333,7 @@ func (p *PostgresCNSIRepository) Save(guid string, cnsi interfaces.CNSIRecord, e } if _, err := p.db.Exec(saveCNSI, guid, cnsi.Name, fmt.Sprintf("%s", cnsi.CNSIType), fmt.Sprintf("%s", cnsi.APIEndpoint), cnsi.AuthorizationEndpoint, cnsi.TokenEndpoint, cnsi.DopplerLoggingEndpoint, cnsi.SkipSSLValidation, - cnsi.ClientId, cipherTextClientSecret, cnsi.SSOAllowed, cnsi.SubType, cnsi.Metadata, cnsi.CreatedBy); err != nil { + cnsi.ClientId, cipherTextClientSecret, cnsi.SSOAllowed, cnsi.SubType, cnsi.Metadata, cnsi.Creator); err != nil { return fmt.Errorf("Unable to Save CNSI record: %v", err) } diff --git a/src/jetstream/repository/interfaces/structs.go b/src/jetstream/repository/interfaces/structs.go index e1887f7dd9..252d6db68f 100644 --- a/src/jetstream/repository/interfaces/structs.go +++ b/src/jetstream/repository/interfaces/structs.go @@ -55,7 +55,7 @@ type CNSIRecord struct { SubType string `json:"sub_type"` Metadata string `json:"metadata"` Local bool `json:"local"` - CreatedBy string `json:"created_by"` + Creator string `json:"creator"` } // ConnectedEndpoint @@ -73,7 +73,7 @@ type ConnectedEndpoint struct { SubType string `json:"sub_type"` EndpointMetadata string `json:"metadata"` Local bool `json:"local"` - CreatedBy string `json:"created_by"` + Creator string `json:"creator"` } const ( From 610b3964508ee134aec02144f830b581ca765a44 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Mon, 1 Feb 2021 13:27:25 +0100 Subject: [PATCH 018/101] Add username to the creator in the front-end (#4753) Signed-off-by: Thomas Quandt --- .../list/list-types/endpoint/base-endpoints-data-source.ts | 5 ++++- .../endpoint/endpoint-card/endpoint-card.component.html | 2 +- src/frontend/packages/store/src/types/endpoint.types.ts | 1 + src/jetstream/info.go | 4 ++++ src/jetstream/repository/interfaces/structs.go | 3 ++- 5 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts index 0dbfe05076..7ed25eead8 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts @@ -123,7 +123,10 @@ export class BaseEndpointsDataSource extends ListDataSource { system_shared_token: false, metricsAvailable: false, sso_allowed: false, - creator: {admin:false} + creator: { + name:'', + admin:false + } }), paginationKey: action.paginationKey, isLocal: true, diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html index a432179f91..9497b4da2a 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html @@ -47,7 +47,7 @@ Created by -
{{ row.creator.admin ? "Admin" : "User" }}
+
{{ row.creator.name }}
diff --git a/src/frontend/packages/store/src/types/endpoint.types.ts b/src/frontend/packages/store/src/types/endpoint.types.ts index 9e09cc9a78..68a77ed2a3 100644 --- a/src/frontend/packages/store/src/types/endpoint.types.ts +++ b/src/frontend/packages/store/src/types/endpoint.types.ts @@ -66,6 +66,7 @@ export interface EndpointUser { // Metadata for the user who created an endpoint export interface CreatorInfo { + name: string; admin: boolean; } diff --git a/src/jetstream/info.go b/src/jetstream/info.go index b3bac96749..f29380bd61 100644 --- a/src/jetstream/info.go +++ b/src/jetstream/info.go @@ -102,6 +102,7 @@ func (p *portalProxy) getInfo(c echo.Context) (*interfaces.Info, error) { // set the creator preemptively as admin, if no id is found endpoint.Creator = &interfaces.CreatorInfo{ + Name: "admin", Admin: true, } @@ -110,6 +111,9 @@ func (p *portalProxy) getInfo(c echo.Context) (*interfaces.Info, error) { u, err := p.StratosAuthService.GetUser(cnsi.Creator) if err == nil { endpoint.Creator.Admin = u.Admin + if u.Admin == false { + endpoint.Creator.Name = u.Name + } } } diff --git a/src/jetstream/repository/interfaces/structs.go b/src/jetstream/repository/interfaces/structs.go index 252d6db68f..982617dc71 100644 --- a/src/jetstream/repository/interfaces/structs.go +++ b/src/jetstream/repository/interfaces/structs.go @@ -195,7 +195,8 @@ type ConnectedUser struct { // CreatorInfo - additional information about the user who created an endpoint type CreatorInfo struct { - Admin bool `json:"admin"` + Name string `json:"name"` + Admin bool `json:"admin"` } type JWTUserTokenInfo struct { From 245b5b3619bece58b2d3d278d60e13efb3f54829 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 4 Feb 2021 09:36:19 +0100 Subject: [PATCH 019/101] Adjust url unique check when creating endpoints (#4753) * Let admins see all endpoints again * Add checkbox to overwrite user-endpoints when admins create a checkpoint * Add OverwriteEndpoints to the HTTP Request * Fix existingEndpoints in git-registration component (was always null) * Change subscription to observable in endpoints page component Signed-off-by: Thomas Quandt --- .../create-endpoint-cf-step-1.component.html | 9 +++- .../create-endpoint-cf-step-1.component.ts | 41 +++++++++++++++--- .../endpoints-page.component.html | 2 +- .../endpoints-page.component.ts | 18 +++++--- .../git-registration.component.html | 8 +++- .../git-registration.component.ts | 43 ++++++++++++++++++- .../store/src/actions/endpoint.actions.ts | 1 + .../store/src/effects/endpoint.effects.ts | 3 +- .../store/src/stratos-action-builders.ts | 3 ++ src/jetstream/cnsi.go | 29 +++++++++---- src/jetstream/plugins/cloudfoundry/main.go | 2 +- .../repository/interfaces/portal_proxy.go | 2 +- .../repository/interfaces/structs.go | 17 ++++---- 13 files changed, 141 insertions(+), 37 deletions(-) diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html index 1a25da9ffb..054718e20d 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html @@ -2,17 +2,22 @@

{{endpoint.definition.label}} Information

+ [appUnique]="registerForm.value.overwriteEndpointsField ? (existingAdminEndpoints | async)?.names : (existingEndpoints | async)?.names"> Name is required Name is not unique + pattern="{{urlValidation}}" + [appUnique]="registerForm.value.overwriteEndpointsField ? (existingAdminEndpoints | async)?.urls : (existingEndpoints | async)?.urls"> URL is required Invalid API URL URL is not unique + Automatically overwrite user endpoints + Skip SSL validation for the endpoint diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts index 73b94605e9..e9e64731d8 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts @@ -1,6 +1,7 @@ import { AfterContentInit, Component, Input } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; +import { StratosCurrentUserPermissions } from '../../../../core/permissions/stratos-user-permissions.checker'; import { Observable } from 'rxjs'; import { filter, map, pairwise } from 'rxjs/operators'; @@ -16,6 +17,12 @@ import { IStepperStep, StepOnNextFunction } from '../../../../shared/components/ import { SnackBarService } from '../../../../shared/services/snackbar.service'; import { ConnectEndpointConfig } from '../../connect.service'; import { getSSOClientRedirectURI } from '../../endpoint-helpers'; +import { SessionService } from '../../../../shared/services/session.service'; + +type EndpointObservable = Observable<{ + names: string[], + urls: string[], +}>; @Component({ selector: 'app-create-endpoint-cf-step-1', @@ -42,10 +49,10 @@ export class CreateEndpointCfStep1Component implements IStepperStep, AfterConten } } - existingEndpoints: Observable<{ - names: string[], - urls: string[], - }>; + overwritePermission: Observable; + + existingEndpoints: EndpointObservable; + existingAdminEndpoints: EndpointObservable; validate: Observable; @@ -63,7 +70,8 @@ export class CreateEndpointCfStep1Component implements IStepperStep, AfterConten constructor( private fb: FormBuilder, activatedRoute: ActivatedRoute, - private snackBarService: SnackBarService + private snackBarService: SnackBarService, + private sessionService: SessionService ) { this.registerForm = this.fb.group({ nameField: ['', [Validators.required]], @@ -73,15 +81,27 @@ export class CreateEndpointCfStep1Component implements IStepperStep, AfterConten // Optional Client ID and Client Secret clientIDField: ['', []], clientSecretField: ['', []], + overwriteEndpointsField: [false, []], }); - this.existingEndpoints = stratosEntityCatalog.endpoint.store.getAll.getPaginationMonitor().currentPage$.pipe( + const currentPage$ = stratosEntityCatalog.endpoint.store.getAll.getPaginationMonitor().currentPage$; + this.existingAdminEndpoints = currentPage$.pipe( + map(endpoints => ({ + names: endpoints.filter(ep => ep.creator.admin).map(ep => ep.name), + urls: endpoints.filter(ep => ep.creator.admin).map(ep => getFullEndpointApiUrl(ep)), + })) + ); + this.existingEndpoints = currentPage$.pipe( map(endpoints => ({ names: endpoints.map(ep => ep.name), urls: endpoints.map(ep => getFullEndpointApiUrl(ep)), })) ); + this.overwritePermission = this.sessionService.userEndpointsEnabled().pipe( + map(enabled => enabled ? [StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT] : []) + ); + const epType = getIdFromRoute(activatedRoute, 'type'); const epSubType = getIdFromRoute(activatedRoute, 'subtype'); this.endpoint = entityCatalog.getEndpoint(epType, epSubType); @@ -102,6 +122,7 @@ export class CreateEndpointCfStep1Component implements IStepperStep, AfterConten this.registerForm.value.clientIDField, this.registerForm.value.clientSecretField, this.registerForm.value.ssoAllowedField, + this.registerForm.value.overwriteEndpointsField, ).pipe( pairwise(), filter(([oldVal, newVal]) => (oldVal.busy && !newVal.busy)), @@ -151,4 +172,12 @@ export class CreateEndpointCfStep1Component implements IStepperStep, AfterConten toggleAdvancedOptions() { this.showAdvancedOptions = !this.showAdvancedOptions; } + + toggleOverwriteEndpoints() { + // wait a tick for validators to adjust to new data in the directive + setTimeout(()=>{ + this.registerForm.controls.nameField.updateValueAndValidity(); + this.registerForm.controls.urlField.updateValueAndValidity(); + }); + } } diff --git a/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.html b/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.html index facd9c4be7..ba1bbe7abb 100644 --- a/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.html +++ b/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.html @@ -1,7 +1,7 @@

Endpoints

- diff --git a/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts b/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts index 29d79a534c..8604677d73 100644 --- a/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts @@ -46,7 +46,7 @@ import { SessionService } from '../../../shared/services/session.service'; }, EndpointListHelper] }) export class EndpointsPageComponent implements AfterViewInit, OnDestroy, OnInit { - public canRegisterEndpoint = [StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT]; + public canRegisterEndpoint: Observable; private healthCheckTimeout: number; public canBackupRestore$: Observable; @@ -89,16 +89,22 @@ export class EndpointsPageComponent implements AfterViewInit, OnDestroy, OnInit first() ).subscribe(); + this.canRegisterEndpoint = this.sessionService.userEndpointsEnabled().pipe( + map(enabled => { + if(enabled){ + return [StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT, StratosCurrentUserPermissions.EDIT_ENDPOINT]; + }else{ + return [StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT] + } + }) + ); + // Is the backup/restore plugin available on the backend? this.canBackupRestore$ = this.store.select(selectSessionData()).pipe( first(), map(sessionData => sessionData?.plugins.backup), - switchMap(enabled => enabled ? currentUserPermissionsService.can(this.canRegisterEndpoint[0]) : of(false)) + switchMap(enabled => enabled ? currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT) : of(false)) ); - - this.sessionService.userEndpointsEnabled().subscribe(enabled => { - if(enabled) this.canRegisterEndpoint.push(StratosCurrentUserPermissions.EDIT_ENDPOINT); - }); } subs: Subscription[] = []; diff --git a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.html b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.html index 64ef2fb0c1..bf3469013f 100644 --- a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.html +++ b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.html @@ -19,18 +19,22 @@

Select the type of {{gitTypes[epSubType].label}} to register

+ [appUnique]="registerForm.value.overwriteEndpointsField ? (existingAdminEndpoints | async)?.names : (existingEndpoints | async)?.names"> Name is required Name is not unique + [appUnique]="registerForm.value.overwriteEndpointsField ? (existingAdminEndpoints | async)?.urls : (existingEndpoints | async)?.urls"> URL is required Invalid API URL URL is not unique + Automatically overwrite user endpoints + Skip SSL validation for the endpoint diff --git a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts index 19cdca1276..5e52efb199 100644 --- a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts +++ b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts @@ -1,6 +1,8 @@ import { Component, OnDestroy } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; +import { StratosCurrentUserPermissions } from 'frontend/packages/core/src/core/permissions/stratos-user-permissions.checker'; +import { SessionService } from 'frontend/packages/core/src/shared/services/session.service'; import { Observable, Subscription } from 'rxjs'; import { filter, first, map, pairwise } from 'rxjs/operators'; @@ -44,6 +46,11 @@ enum GitTypeKeys { GITLAB_ENTERPRISE = 'githubenterprize', } +type EndpointObservable = Observable<{ + names: string[], + urls: string[], +}>; + @Component({ selector: 'app-git-registration', templateUrl: './git-registration.component.html', @@ -65,12 +72,18 @@ export class GitRegistrationComponent implements OnDestroy { urlValidation: string; + overwritePermission: Observable; + + existingEndpoints: EndpointObservable; + existingAdminEndpoints: EndpointObservable; + constructor( gitSCMService: GitSCMService, activatedRoute: ActivatedRoute, private fb: FormBuilder, private snackBarService: SnackBarService, private endpointsService: EndpointsService, + private sessionService: SessionService, ) { this.epSubType = getIdFromRoute(activatedRoute, 'subtype'); const githubLabel = entityCatalog.getEndpoint(GIT_ENDPOINT_TYPE, GIT_ENDPOINT_SUB_TYPES.GITHUB).definition.label || 'Github'; @@ -130,6 +143,24 @@ export class GitRegistrationComponent implements OnDestroy { } }; + const currentPage$ = stratosEntityCatalog.endpoint.store.getAll.getPaginationMonitor().currentPage$; + this.existingAdminEndpoints = currentPage$.pipe( + map(endpoints => ({ + names: endpoints.filter(ep => ep.creator.admin).map(ep => ep.name), + urls: endpoints.filter(ep => ep.creator.admin).map(ep => getFullEndpointApiUrl(ep)), + })) + ); + this.existingEndpoints = currentPage$.pipe( + map(endpoints => ({ + names: endpoints.map(ep => ep.name), + urls: endpoints.map(ep => getFullEndpointApiUrl(ep)), + })) + ); + + this.overwritePermission = this.sessionService.userEndpointsEnabled().pipe( + map(enabled => enabled ? [StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT] : []) + ); + // Check the endpoints and turn off any options for endpoints that are already registered this.endpointsService.endpoints$.pipe(first()).subscribe(eps => { Object.values(this.gitTypes[this.epSubType].types).forEach(type => { @@ -151,6 +182,7 @@ export class GitRegistrationComponent implements OnDestroy { nameField: ['', [Validators.required]], urlField: ['', [Validators.required]], skipSllField: [false, []], + overwriteEndpointsField: [false, []], }); this.updateType(); @@ -192,8 +224,9 @@ export class GitRegistrationComponent implements OnDestroy { const skipSSL = this.registerForm.controls.nameField.value && this.registerForm.controls.urlField.value ? this.registerForm.controls.skipSllField.value : false; + const overwriteEndpoints = this.registerForm.controls.overwriteEndpointsField.value; - return stratosEntityCatalog.endpoint.api.register(GIT_ENDPOINT_TYPE, this.epSubType, name, url, skipSSL, '', '', false) + return stratosEntityCatalog.endpoint.api.register(GIT_ENDPOINT_TYPE, this.epSubType, name, url, skipSSL, '', '', false, overwriteEndpoints) .pipe( pairwise(), filter(([oldVal, newVal]) => (oldVal.busy && !newVal.busy)), @@ -228,4 +261,12 @@ export class GitRegistrationComponent implements OnDestroy { const ready = urlTrimmed[urlTrimmed.length - 1] === '/' ? urlTrimmed.substring(0, urlTrimmed.length - 1) : urlTrimmed; return ready + '/' + defn.urlSuffix; } + + toggleOverwriteEndpoints() { + // wait a tick for validators to adjust to new data in the directive + setTimeout(()=>{ + this.registerForm.controls.nameField.updateValueAndValidity(); + this.registerForm.controls.urlField.updateValueAndValidity(); + }); + } } diff --git a/src/frontend/packages/store/src/actions/endpoint.actions.ts b/src/frontend/packages/store/src/actions/endpoint.actions.ts index ac3ccdddae..26b11a231a 100644 --- a/src/frontend/packages/store/src/actions/endpoint.actions.ts +++ b/src/frontend/packages/store/src/actions/endpoint.actions.ts @@ -215,6 +215,7 @@ export class RegisterEndpoint extends SingleBaseEndpointAction { public clientID = '', public clientSecret = '', public ssoAllowed: boolean, + public overwriteEndpoints: boolean, ) { super( REGISTER_ENDPOINTS, diff --git a/src/frontend/packages/store/src/effects/endpoint.effects.ts b/src/frontend/packages/store/src/effects/endpoint.effects.ts index af1a2d7757..f39b06c995 100644 --- a/src/frontend/packages/store/src/effects/endpoint.effects.ts +++ b/src/frontend/packages/store/src/effects/endpoint.effects.ts @@ -194,7 +194,8 @@ export class EndpointsEffect { skip_ssl_validation: action.skipSslValidation ? 'true' : 'false', cnsi_client_id: action.clientID, cnsi_client_secret: action.clientSecret, - sso_allowed: action.ssoAllowed ? 'true' : 'false' + sso_allowed: action.ssoAllowed ? 'true' : 'false', + overwrite_endpoints: action.overwriteEndpoints ? 'true' : 'false' }; // Do not include sub_type in HttpParams if it doesn't exist (falsies get stringified and sent) if (action.endpointSubType) { diff --git a/src/frontend/packages/store/src/stratos-action-builders.ts b/src/frontend/packages/store/src/stratos-action-builders.ts index 39dc68a725..d2c869fe1f 100644 --- a/src/frontend/packages/store/src/stratos-action-builders.ts +++ b/src/frontend/packages/store/src/stratos-action-builders.ts @@ -60,6 +60,7 @@ export interface EndpointActionBuilder extends OrchestratedActionBuilders { clientID?: string, clientSecret?: string, ssoAllowed?: boolean, + overwriteEndpointsField?: boolean, ) => RegisterEndpoint; update: ( guid: string, @@ -104,6 +105,7 @@ export const endpointActionBuilder: EndpointActionBuilder = { clientID?: string, clientSecret?: string, ssoAllowed?: boolean, + overwriteEndpoints?: boolean, ) => new RegisterEndpoint( endpointType, endpointSubType, @@ -113,6 +115,7 @@ export const endpointActionBuilder: EndpointActionBuilder = { clientID, clientSecret, ssoAllowed, + overwriteEndpoints, ), update: ( guid: string, diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 30a138c124..1419782501 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -62,6 +62,12 @@ func (p *portalProxy) RegisterEndpoint(c echo.Context, fetchInfo interfaces.Info cnsiClientSecret := params.CNSIClientSecret subType := params.SubType + overwriteEndpoints, err := strconv.ParseBool(params.OverwriteEndpoints) + if err != nil { + // default to false + overwriteEndpoints = false + } + if cnsiClientId == "" { cnsiClientId = p.GetConfig().CFClient cnsiClientSecret = p.GetConfig().CFClientSecret @@ -76,7 +82,7 @@ func (p *portalProxy) RegisterEndpoint(c echo.Context, fetchInfo interfaces.Info } uaaUserId := sessionValue.(string) - newCNSI, err := p.DoRegisterEndpoint(params.CNSIName, params.APIEndpoint, skipSSLValidation, cnsiClientId, cnsiClientSecret, uaaUserId, ssoAllowed, subType, fetchInfo) + newCNSI, err := p.DoRegisterEndpoint(params.CNSIName, params.APIEndpoint, skipSSLValidation, cnsiClientId, cnsiClientSecret, uaaUserId, ssoAllowed, subType, overwriteEndpoints, fetchInfo) if err != nil { return err } @@ -85,7 +91,7 @@ func (p *portalProxy) RegisterEndpoint(c echo.Context, fetchInfo interfaces.Info return nil } -func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, skipSSLValidation bool, clientId string, clientSecret string, userId string, ssoAllowed bool, subType string, fetchInfo interfaces.InfoFunc) (interfaces.CNSIRecord, error) { +func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, skipSSLValidation bool, clientId string, clientSecret string, userId string, ssoAllowed bool, subType string, overwriteEndpoints bool, fetchInfo interfaces.InfoFunc) (interfaces.CNSIRecord, error) { if len(cnsiName) == 0 || len(apiEndpoint) == 0 { return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( @@ -146,9 +152,8 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk // check if we've already got this APIEndpoint from a admin in this DB for _, duplicate := range duplicateEndpoints { - // filter out all endpoints created by users creatorRecord, err := p.StratosAuthService.GetUser(duplicate.Creator) - if len(duplicate.Creator) != 0 || creatorRecord.Admin == true || err != nil { + if len(duplicate.Creator) == 0 || creatorRecord.Admin == true || err != nil { return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( http.StatusBadRequest, "Can not register same admin endpoint multiple times", @@ -157,7 +162,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk } } - if currentCreator.Admin == true { + if currentCreator.Admin == true && overwriteEndpoints == true { // remove all endpoints with same APIEndpoint for _, duplicate := range duplicateEndpoints { p.doUnregisterCluster(duplicate.GUID) @@ -255,10 +260,18 @@ func (p *portalProxy) buildCNSIList(c echo.Context) ([]*interfaces.CNSIRecord, e if err != nil { return nil, err } - return p.ListAdminEndpoints(userID.(string)) - } else { - return p.ListEndpoints() + + u, err := p.StratosAuthService.GetUser(userID.(string)) + if err != nil { + return nil, err + } + + if u.Admin == false { + return p.ListAdminEndpoints(userID.(string)) + } } + + return p.ListEndpoints() } func (p *portalProxy) ListEndpoints() ([]*interfaces.CNSIRecord, error) { diff --git a/src/jetstream/plugins/cloudfoundry/main.go b/src/jetstream/plugins/cloudfoundry/main.go index 59c7e96e7d..4f5918d5bb 100644 --- a/src/jetstream/plugins/cloudfoundry/main.go +++ b/src/jetstream/plugins/cloudfoundry/main.go @@ -127,7 +127,7 @@ func (c *CloudFoundrySpecification) cfLoginHook(context echo.Context) error { log.Infof("Auto-registering cloud foundry endpoint %s as \"%s\"", cfAPI, autoRegName) // Auto-register the Cloud Foundry - cfCnsi, err = c.portalProxy.DoRegisterEndpoint(autoRegName, cfAPI, true, c.portalProxy.GetConfig().CFClient, c.portalProxy.GetConfig().CFClientSecret, userGUID, false, "", cfEndpointSpec.Info) + cfCnsi, err = c.portalProxy.DoRegisterEndpoint(autoRegName, cfAPI, true, c.portalProxy.GetConfig().CFClient, c.portalProxy.GetConfig().CFClientSecret, userGUID, false, "", false, cfEndpointSpec.Info) if err != nil { log.Errorf("Could not auto-register Cloud Foundry endpoint: %v", err) return nil diff --git a/src/jetstream/repository/interfaces/portal_proxy.go b/src/jetstream/repository/interfaces/portal_proxy.go index 1f7b396368..309f236ca4 100644 --- a/src/jetstream/repository/interfaces/portal_proxy.go +++ b/src/jetstream/repository/interfaces/portal_proxy.go @@ -14,7 +14,7 @@ type PortalProxy interface { GetHttpClient(skipSSLValidation bool) http.Client GetHttpClientForRequest(req *http.Request, skipSSLValidation bool) http.Client RegisterEndpoint(c echo.Context, fetchInfo InfoFunc) error - DoRegisterEndpoint(cnsiName string, apiEndpoint string, skipSSLValidation bool, clientId string, clientSecret string, userId string, ssoAllowed bool, subType string, fetchInfo InfoFunc) (CNSIRecord, error) + DoRegisterEndpoint(cnsiName string, apiEndpoint string, skipSSLValidation bool, clientId string, clientSecret string, userId string, ssoAllowed bool, subType string, overwriteEndpoints bool, fetchInfo InfoFunc) (CNSIRecord, error) GetEndpointTypeSpec(typeName string) (EndpointPlugin, error) // Auth diff --git a/src/jetstream/repository/interfaces/structs.go b/src/jetstream/repository/interfaces/structs.go index 982617dc71..bd636d8d5a 100644 --- a/src/jetstream/repository/interfaces/structs.go +++ b/src/jetstream/repository/interfaces/structs.go @@ -425,14 +425,15 @@ type LoginToCNSIParams struct { } type RegisterEndpointParams struct { - EndpointType string `json:"endpoint_type" form:"endpoint_type" query:"endpoint_type"` - CNSIName string `json:"cnsi_name" form:"cnsi_name" query:"cnsi_name"` - APIEndpoint string `json:"api_endpoint" form:"api_endpoint" query:"api_endpoint"` - SkipSSLValidation string `json:"skip_ssl_validation" form:"skip_ssl_validation" query:"skip_ssl_validation"` - SSOAllowed string `json:"sso_allowed" form:"sso_allowed" query:"sso_allowed"` - CNSIClientID string `json:"cnsi_client_id" form:"cnsi_client_id" query:"cnsi_client_id"` - CNSIClientSecret string `json:"cnsi_client_secret" form:"cnsi_client_secret" query:"cnsi_client_secret"` - SubType string `json:"sub_type" form:"sub_type" query:"sub_type"` + EndpointType string `json:"endpoint_type" form:"endpoint_type" query:"endpoint_type"` + CNSIName string `json:"cnsi_name" form:"cnsi_name" query:"cnsi_name"` + APIEndpoint string `json:"api_endpoint" form:"api_endpoint" query:"api_endpoint"` + SkipSSLValidation string `json:"skip_ssl_validation" form:"skip_ssl_validation" query:"skip_ssl_validation"` + SSOAllowed string `json:"sso_allowed" form:"sso_allowed" query:"sso_allowed"` + CNSIClientID string `json:"cnsi_client_id" form:"cnsi_client_id" query:"cnsi_client_id"` + CNSIClientSecret string `json:"cnsi_client_secret" form:"cnsi_client_secret" query:"cnsi_client_secret"` + SubType string `json:"sub_type" form:"sub_type" query:"sub_type"` + OverwriteEndpoints string `json:"overwrite_endpoints" form:"overwrite_endpoints" query:"overwrite_endpoints"` } type UpdateEndpointParams struct { From f0a53bbfd80d0dfb1fd34e9f9f03365e84065656 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Fri, 5 Feb 2021 12:08:07 +0100 Subject: [PATCH 020/101] Prevent admins from connecting to user endpoints (#4753) Signed-off-by: Thomas Quandt --- .../endpoint/endpoint-list.helpers.ts | 22 ++++++++++++++----- src/jetstream/authcnsi.go | 22 ++++++++++++++----- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts index de611bdde7..1b1cdccf2f 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts @@ -135,11 +135,23 @@ export class EndpointListHelper { }, label: 'Connect', description: '', - createVisible: (row$: Observable) => row$.pipe(map(row => { - const endpoint = entityCatalog.getEndpoint(row.cnsi_type, row.sub_type); - const ep = endpoint ? endpoint.definition : { unConnectable: false }; - return !ep.unConnectable && row.connectionStatus === 'disconnected'; - })) + createVisible: (row$: Observable) => { + return combineLatest([ + this.sessionService.userEndpointsEnabled(), + this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT), + row$ + ]).pipe( + map(([userEndpointsEnabled, isAdmin, row]) => { + if(userEndpointsEnabled && !row.creator.admin && isAdmin){ + return false; + }else{ + const endpoint = entityCatalog.getEndpoint(row.cnsi_type, row.sub_type); + const ep = endpoint ? endpoint.definition : { unConnectable: false }; + return !ep.unConnectable && row.connectionStatus === 'disconnected'; + } + }) + ); + } }, { action: (item) => { diff --git a/src/jetstream/authcnsi.go b/src/jetstream/authcnsi.go index 51ceeaece0..64c73aa786 100644 --- a/src/jetstream/authcnsi.go +++ b/src/jetstream/authcnsi.go @@ -149,14 +149,26 @@ func (p *portalProxy) DoLoginToCNSI(c echo.Context, cnsiGUID string, systemShare return nil, echo.NewHTTPError(http.StatusUnauthorized, "Could not find correct session value") } - // Register as a system endpoint? - if systemSharedToken { - // User needs to be an admin - user, err := p.StratosAuthService.GetUser(userID) + user, err := p.StratosAuthService.GetUser(userID) + if err != nil { + return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - could not check user") + } + + // admins are note allowed to connect to user created endpoints + if p.GetConfig().EnableUserEndpoints && len(cnsiRecord.Creator) > 0 { + cnsiUser, err := p.StratosAuthService.GetUser(cnsiRecord.Creator) if err != nil { - return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect System Shared endpoint - could not check user") + return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - endpoint creator has no account") } + if !cnsiUser.Admin && user.Admin { + return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - admins are not allowed to connect to user created endpoints") + } + } + + // Register as a system endpoint? + if systemSharedToken { + // User needs to be an admin if !user.Admin { return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect System Shared endpoint - user is not an administrator") } From 3f9b69bcb28493d564cab06f0b72c0583755ce93 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Mon, 8 Feb 2021 14:42:22 +0100 Subject: [PATCH 021/101] Exclude endpoint admins from no-endpoint-no-admin page (#4753) Signed-off-by: Thomas Quandt --- .../packages/core/src/core/endpoints.service.ts | 12 ++++++++---- src/frontend/packages/core/src/core/user.service.ts | 8 ++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/frontend/packages/core/src/core/endpoints.service.ts b/src/frontend/packages/core/src/core/endpoints.service.ts index f7860ba3c3..08559e32fb 100644 --- a/src/frontend/packages/core/src/core/endpoints.service.ts +++ b/src/frontend/packages/core/src/core/endpoints.service.ts @@ -14,6 +14,7 @@ import { endpointEntitiesSelector, endpointStatusSelector } from '../../../store import { EndpointModel, EndpointState } from '../../../store/src/types/endpoint.types'; import { IEndpointFavMetadata, UserFavorite } from '../../../store/src/types/user-favorites.types'; import { endpointHasMetricsByAvailable } from '../features/endpoints/endpoint-helpers'; +import { SessionService } from '../shared/services/session.service'; import { EndpointHealthChecks } from './endpoints-health-checks'; import { UserService } from './user.service'; @@ -50,7 +51,8 @@ export class EndpointsService implements CanActivate { constructor( private store: Store, private userService: UserService, - private endpointHealthChecks: EndpointHealthChecks + private endpointHealthChecks: EndpointHealthChecks, + private sessionService: SessionService ) { this.endpoints$ = store.select(endpointEntitiesSelector); this.haveRegistered$ = this.endpoints$.pipe(map(endpoints => !!Object.keys(endpoints).length)); @@ -99,17 +101,19 @@ export class EndpointsService implements CanActivate { this.haveRegistered$, this.haveConnected$, this.userService.isAdmin$, + this.userService.isEndpointAdmin$, + this.sessionService.userEndpointsEnabled(), this.disablePersistenceFeatures$ ), - map(([state, haveRegistered, haveConnected, isAdmin, disablePersistenceFeatures] - : [[AuthState, EndpointState], boolean, boolean, boolean, boolean]) => { + map(([state, haveRegistered, haveConnected, isAdmin, isEndpointAdmin, userEndpointsEnabled, disablePersistenceFeatures] + : [[AuthState, EndpointState], boolean, boolean, boolean, boolean, boolean, boolean]) => { const [authState] = state; if (authState.sessionData.valid) { // Redirect to endpoints if there's no connected endpoints let redirect: string; if (!disablePersistenceFeatures) { if (!haveRegistered) { - redirect = isAdmin ? '/endpoints' : '/noendpoints'; + redirect = isAdmin || (userEndpointsEnabled && isEndpointAdmin) ? '/endpoints' : '/noendpoints'; } else if (!haveConnected) { redirect = '/endpoints'; } diff --git a/src/frontend/packages/core/src/core/user.service.ts b/src/frontend/packages/core/src/core/user.service.ts index 9add961da8..956aef5e33 100644 --- a/src/frontend/packages/core/src/core/user.service.ts +++ b/src/frontend/packages/core/src/core/user.service.ts @@ -10,10 +10,18 @@ import { AuthOnlyAppState } from '../../../store/src/app-state'; export class UserService { isAdmin$: Observable; + isEndpointAdmin$: Observable; constructor(store: Store) { this.isAdmin$ = store.select(s => s.auth).pipe( map((auth: AuthState) => auth.sessionData && auth.sessionData.user && auth.sessionData.user.admin)); + + this.isEndpointAdmin$ = store.select(s => s.auth).pipe( + map((auth: AuthState) => { + return (auth.sessionData + && auth.sessionData.user + && auth.sessionData.user.scopes.find(e => e === "stratos.endpointadmin") !== undefined); + })); } } From 8bbb26c00c8fea52d22510acbaec912e7cfdac5a Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Tue, 9 Feb 2021 11:47:57 +0100 Subject: [PATCH 022/101] Disallow sharing of user-endpoints through invites (#4753) Signed-off-by: Thomas Quandt --- .../cf/user-invites/user-invite.service.ts | 12 ++++--- src/jetstream/info.go | 1 + src/jetstream/plugins/userinvite/admin.go | 5 +++ src/jetstream/plugins/userinvite/auth.go | 36 +++++++++++++++++++ 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/frontend/packages/cloud-foundry/src/features/cf/user-invites/user-invite.service.ts b/src/frontend/packages/cloud-foundry/src/features/cf/user-invites/user-invite.service.ts index 6ebb948cbc..e9d5fab645 100644 --- a/src/frontend/packages/cloud-foundry/src/features/cf/user-invites/user-invite.service.ts +++ b/src/frontend/packages/cloud-foundry/src/features/cf/user-invites/user-invite.service.ts @@ -143,13 +143,15 @@ export class UserInviteService { map(v => v.entity.metadata && v.entity.metadata.userInviteAllowed === 'true') ); - this.canConfigure$ = combineLatest( + this.canConfigure$ = combineLatest([ waitForCFPermissions(this.store, this.activeRouteCfOrgSpace.cfGuid), - this.store.select('auth') - ).pipe( - map(([cf, auth]) => + this.store.select('auth'), + cfEndpointService.endpoint$ + ]).pipe( + map(([cf, auth, endpoint]) => cf.global.isAdmin && - auth.sessionData['plugin-config'] && auth.sessionData['plugin-config'].userInvitationsEnabled === 'true') + auth.sessionData['plugin-config'] && auth.sessionData['plugin-config'].userInvitationsEnabled === 'true' && + endpoint.entity.creator.admin) ); } diff --git a/src/jetstream/info.go b/src/jetstream/info.go index f29380bd61..afc3833a40 100644 --- a/src/jetstream/info.go +++ b/src/jetstream/info.go @@ -111,6 +111,7 @@ func (p *portalProxy) getInfo(c echo.Context) (*interfaces.Info, error) { u, err := p.StratosAuthService.GetUser(cnsi.Creator) if err == nil { endpoint.Creator.Admin = u.Admin + // dont set username of admins for security reasons if u.Admin == false { endpoint.Creator.Name = u.Name } diff --git a/src/jetstream/plugins/userinvite/admin.go b/src/jetstream/plugins/userinvite/admin.go index 72363560dc..b8cde28a39 100644 --- a/src/jetstream/plugins/userinvite/admin.go +++ b/src/jetstream/plugins/userinvite/admin.go @@ -79,6 +79,11 @@ func (invite *UserInvite) configure(c echo.Context) error { ) } + _, err := invite.checkEndpointCreator(cfGUID, c) + if err != nil { + return err + } + uaaRecord, _, err := invite.RefreshToken(cfGUID, clientID, clientSecret) if err != nil { return err diff --git a/src/jetstream/plugins/userinvite/auth.go b/src/jetstream/plugins/userinvite/auth.go index 592b0f8561..d10740b1f1 100644 --- a/src/jetstream/plugins/userinvite/auth.go +++ b/src/jetstream/plugins/userinvite/auth.go @@ -119,6 +119,42 @@ func (invite *UserInvite) refreshToken(clientID, clientSecret string, endpoint i return &uaaResponse, tokenRecord, nil } +// Check that there is an endpoint with the specified ID and if the creator is an admin +func (invite *UserInvite) checkEndpointCreator(cfGUID string, c echo.Context) (interfaces.CNSIRecord, error) { + endpoint, err := invite.portalProxy.GetCNSIRecord(cfGUID) + if err != nil { + // Could find the endpoint + return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( + http.StatusBadRequest, + "Can not find enpoint", + "Can not find enpoint: %s", cfGUID, + ) + } + + if len(endpoint.Creator) > 0 { + stratosAuthService := invite.portalProxy.GetStratosAuthService() + + creator, err := stratosAuthService.GetUser(endpoint.Creator) + if err != nil { + return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( + http.StatusBadRequest, + "Can not find creator account", + "Can not find creator account: %s", endpoint.Creator, + ) + } + + if !creator.Admin { + return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( + http.StatusBadRequest, + "Not an admin endpoint", + "Not an admin endpoint: %s", cfGUID, + ) + } + } + + return endpoint, nil +} + func (invite *UserInvite) checkEndpoint(cfGUID string) (interfaces.CNSIRecord, error) { // Check that there is an endpoint with the specified ID and that it is a Cloud Foundry endpoint endpoint, err := invite.portalProxy.GetCNSIRecord(cfGUID) From cfa200eb83158706f96dca37bc132e5c83f7f3da Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 11 Feb 2021 09:41:13 +0100 Subject: [PATCH 023/101] Change cnsi gui generation and adjust (#4753) * Change AutoRegisterEndpoint to always create as an anonymous admin * Adjust GetCNSIRecordByEndpoint to search for admin created endpoints only * Revert change in cnsiRecordExists to now check the url again instead guid Signed-off-by: Thomas Quandt --- src/jetstream/cnsi.go | 80 ++++++++++++------- src/jetstream/plugins/cloudfoundry/main.go | 4 +- .../repository/interfaces/portal_proxy.go | 2 +- 3 files changed, 54 insertions(+), 32 deletions(-) diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 1419782501..f81fcf6b10 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -111,16 +111,23 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk "Failed to get API Endpoint: %v", err) } - h := sha1.New() - if p.GetConfig().EnableUserEndpoints == true { - h.Write([]byte(apiEndpointURL.String() + userId)) + isAdmin := false + + if len(userId) == 0 { + isAdmin = true } else { - h.Write([]byte(apiEndpointURL.String())) + currentCreator, err := p.StratosAuthService.GetUser(userId) + if err != nil { + return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( + http.StatusInternalServerError, + "Failed to get user information", + "Failed to get user information: %v", err) + } + isAdmin = currentCreator.Admin } - guid := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) // check if we've already got this endpoint in the DB - ok := p.cnsiRecordExists(guid) + ok := p.adminCNSIRecordExists(apiEndpoint) if ok { // a record with the same api endpoint was found return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( @@ -130,16 +137,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk ) } - if p.GetConfig().EnableUserEndpoints == true { - // is the current user an admin -> delete user endpoints? - currentCreator, err := p.StratosAuthService.GetUser(userId) - if err != nil { - return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( - http.StatusInternalServerError, - "Failed to get user information", - "Failed to get user information: %v", err) - } - + if p.GetConfig().EnableUserEndpoints { // get all endpoints determined by the APIEndpoint duplicateEndpoints, err := p.listCNSIByAPIEndpoint(apiEndpoint) if err != nil { @@ -153,7 +151,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk // check if we've already got this APIEndpoint from a admin in this DB for _, duplicate := range duplicateEndpoints { creatorRecord, err := p.StratosAuthService.GetUser(duplicate.Creator) - if len(duplicate.Creator) == 0 || creatorRecord.Admin == true || err != nil { + if len(duplicate.Creator) == 0 || (err == nil && creatorRecord.Admin == true) { return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( http.StatusBadRequest, "Can not register same admin endpoint multiple times", @@ -162,7 +160,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk } } - if currentCreator.Admin == true && overwriteEndpoints == true { + if isAdmin && overwriteEndpoints { // remove all endpoints with same APIEndpoint for _, duplicate := range duplicateEndpoints { p.doUnregisterCluster(duplicate.GUID) @@ -170,6 +168,16 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk } } + h := sha1.New() + + if p.GetConfig().EnableUserEndpoints && !isAdmin { + h.Write([]byte(apiEndpointURL.String() + userId)) + } else { + h.Write([]byte(apiEndpointURL.String())) + } + + guid := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) + newCNSI, _, err := fetchInfo(apiEndpoint, skipSSLValidation) if err != nil { if ok, detail := isSSLRelatedError(err); ok { @@ -193,7 +201,10 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk newCNSI.ClientSecret = clientSecret newCNSI.SSOAllowed = ssoAllowed newCNSI.SubType = subType - if p.GetConfig().EnableUserEndpoints == true { + + // don't save the creator if it's an admin to not break legacy features like GetCNSIRecordByEndpoint + // once they have been updated, it's safe to save admin ids + if p.GetConfig().EnableUserEndpoints && !isAdmin { newCNSI.Creator = userId } @@ -499,30 +510,41 @@ func (p *portalProxy) GetCNSIRecord(guid string) (interfaces.CNSIRecord, error) return rec, nil } -func (p *portalProxy) GetCNSIRecordByEndpoint(endpoint string) (interfaces.CNSIRecord, error) { +func (p *portalProxy) GetAdminCNSIRecordByEndpoint(endpoint string) (interfaces.CNSIRecord, error) { log.Debug("GetCNSIRecordByEndpoint") - var rec interfaces.CNSIRecord + var rec *interfaces.CNSIRecord - cnsiRepo, err := p.GetStoreFactory().EndpointStore() + endpointList, err := p.listCNSIByAPIEndpoint(endpoint) if err != nil { - return rec, err + return interfaces.CNSIRecord{}, err } - rec, err = cnsiRepo.FindByAPIEndpoint(endpoint, p.Config.EncryptionKeyInBytes) - if err != nil { - return rec, err + // search for endpoint created by an admin + for _, endpoint := range endpointList { + if len(endpoint.Creator) == 0 { + rec = endpoint + } else { + creatorRecord, err := p.StratosAuthService.GetUser(endpoint.Creator) + if err == nil && creatorRecord.Admin == true { + rec = endpoint + } + } + } + + if rec == nil { + return interfaces.CNSIRecord{}, fmt.Errorf("Can not find admin CNSIRecord by given endpoint") } // Ensure that trailing slash is removed from the API Endpoint rec.APIEndpoint.Path = strings.TrimRight(rec.APIEndpoint.Path, "/") - return rec, nil + return *rec, nil } -func (p *portalProxy) cnsiRecordExists(guid string) bool { +func (p *portalProxy) adminCNSIRecordExists(apiEndpoint string) bool { log.Debug("cnsiRecordExists") - _, err := p.GetCNSIRecord(guid) + _, err := p.GetAdminCNSIRecordByEndpoint(apiEndpoint) return err == nil } diff --git a/src/jetstream/plugins/cloudfoundry/main.go b/src/jetstream/plugins/cloudfoundry/main.go index 4f5918d5bb..8b78c9d780 100644 --- a/src/jetstream/plugins/cloudfoundry/main.go +++ b/src/jetstream/plugins/cloudfoundry/main.go @@ -127,7 +127,7 @@ func (c *CloudFoundrySpecification) cfLoginHook(context echo.Context) error { log.Infof("Auto-registering cloud foundry endpoint %s as \"%s\"", cfAPI, autoRegName) // Auto-register the Cloud Foundry - cfCnsi, err = c.portalProxy.DoRegisterEndpoint(autoRegName, cfAPI, true, c.portalProxy.GetConfig().CFClient, c.portalProxy.GetConfig().CFClientSecret, userGUID, false, "", false, cfEndpointSpec.Info) + cfCnsi, err = c.portalProxy.DoRegisterEndpoint(autoRegName, cfAPI, true, c.portalProxy.GetConfig().CFClient, c.portalProxy.GetConfig().CFClientSecret, "", false, "", false, cfEndpointSpec.Info) if err != nil { log.Errorf("Could not auto-register Cloud Foundry endpoint: %v", err) return nil @@ -178,7 +178,7 @@ func (c *CloudFoundrySpecification) fetchAutoRegisterEndpoint() (string, interfa return "", interfaces.CNSIRecord{}, nil } // Error is populated if there was an error OR there was no record - cfCnsi, err := c.portalProxy.GetCNSIRecordByEndpoint(cfAPI) + cfCnsi, err := c.portalProxy.GetAdminCNSIRecordByEndpoint(cfAPI) return cfAPI, cfCnsi, err } diff --git a/src/jetstream/repository/interfaces/portal_proxy.go b/src/jetstream/repository/interfaces/portal_proxy.go index 309f236ca4..c31e21bd26 100644 --- a/src/jetstream/repository/interfaces/portal_proxy.go +++ b/src/jetstream/repository/interfaces/portal_proxy.go @@ -36,7 +36,7 @@ type PortalProxy interface { // Expose internal portal proxy records to extensions GetCNSIRecord(guid string) (CNSIRecord, error) - GetCNSIRecordByEndpoint(endpoint string) (CNSIRecord, error) + GetAdminCNSIRecordByEndpoint(endpoint string) (CNSIRecord, error) GetCNSITokenRecord(cnsiGUID string, userGUID string) (TokenRecord, bool) GetCNSITokenRecordWithDisconnected(cnsiGUID string, userGUID string) (TokenRecord, bool) GetCNSIUser(cnsiGUID string, userGUID string) (*ConnectedUser, bool) From d256885185625afdd5cc546538efdb0a1255ced3 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 11 Feb 2021 10:15:19 +0100 Subject: [PATCH 024/101] Fix endpoint creation check and update debug logs (#4753) Signed-off-by: Thomas Quandt --- src/jetstream/cnsi.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index f81fcf6b10..a2039e4202 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -92,6 +92,7 @@ func (p *portalProxy) RegisterEndpoint(c echo.Context, fetchInfo interfaces.Info } func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, skipSSLValidation bool, clientId string, clientSecret string, userId string, ssoAllowed bool, subType string, overwriteEndpoints bool, fetchInfo interfaces.InfoFunc) (interfaces.CNSIRecord, error) { + log.Debug("DoRegisterEndpoint") if len(cnsiName) == 0 || len(apiEndpoint) == 0 { return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( @@ -113,6 +114,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk isAdmin := false + // anonymous admin? if len(userId) == 0 { isAdmin = true } else { @@ -148,7 +150,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk err) } - // check if we've already got this APIEndpoint from a admin in this DB + // check if we've already got this APIEndpoint in this DB for _, duplicate := range duplicateEndpoints { creatorRecord, err := p.StratosAuthService.GetUser(duplicate.Creator) if len(duplicate.Creator) == 0 || (err == nil && creatorRecord.Admin == true) { @@ -158,9 +160,16 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk "Can not register same admin endpoint multiple times", ) } + if duplicate.Creator == userId && !overwriteEndpoints { + return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( + http.StatusBadRequest, + "Can not register same endpoint multiple times", + "Can not register same endpoint multiple times", + ) + } } - if isAdmin && overwriteEndpoints { + if isAdmin { // remove all endpoints with same APIEndpoint for _, duplicate := range duplicateEndpoints { p.doUnregisterCluster(duplicate.GUID) @@ -251,6 +260,7 @@ func (p *portalProxy) unregisterCluster(c echo.Context) error { } func (p *portalProxy) doUnregisterCluster(cnsiGUID string) error { + log.Debug("doUnregisterCluster") // Should check for errors? p.unsetCNSIRecord(cnsiGUID) @@ -511,7 +521,7 @@ func (p *portalProxy) GetCNSIRecord(guid string) (interfaces.CNSIRecord, error) } func (p *portalProxy) GetAdminCNSIRecordByEndpoint(endpoint string) (interfaces.CNSIRecord, error) { - log.Debug("GetCNSIRecordByEndpoint") + log.Debug("GetAdminCNSIRecordByEndpoint") var rec *interfaces.CNSIRecord endpointList, err := p.listCNSIByAPIEndpoint(endpoint) @@ -542,7 +552,7 @@ func (p *portalProxy) GetAdminCNSIRecordByEndpoint(endpoint string) (interfaces. } func (p *portalProxy) adminCNSIRecordExists(apiEndpoint string) bool { - log.Debug("cnsiRecordExists") + log.Debug("adminCNSIRecordExists") _, err := p.GetAdminCNSIRecordByEndpoint(apiEndpoint) return err == nil From 124abfe2b0c878a873d7c808a13bcea5cb4c60fe Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 11 Feb 2021 16:52:39 +0100 Subject: [PATCH 025/101] Change endpoints env var from boolean to enum (#4753) * Change EnableUserEndpoints to UserEndpointsEnabled and make it enum * If admins see user endpoints depend now on enum * Users only see their own endpoints if flag explicitly set to enabled Signed-off-by: Thomas Quandt --- src/jetstream/authcnsi.go | 3 +- src/jetstream/cnsi.go | 24 ++++++++----- src/jetstream/info.go | 2 +- src/jetstream/main.go | 2 +- .../repository/interfaces/config/config.go | 36 +++++++++++++++++++ .../repository/interfaces/structs.go | 8 ++--- 6 files changed, 59 insertions(+), 16 deletions(-) diff --git a/src/jetstream/authcnsi.go b/src/jetstream/authcnsi.go index 64c73aa786..7f7e92445d 100644 --- a/src/jetstream/authcnsi.go +++ b/src/jetstream/authcnsi.go @@ -12,6 +12,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" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/tokens" ) @@ -155,7 +156,7 @@ func (p *portalProxy) DoLoginToCNSI(c echo.Context, cnsiGUID string, systemShare } // admins are note allowed to connect to user created endpoints - if p.GetConfig().EnableUserEndpoints && len(cnsiRecord.Creator) > 0 { + if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled && len(cnsiRecord.Creator) > 0 { cnsiUser, err := p.StratosAuthService.GetUser(cnsiRecord.Creator) if err != nil { return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - endpoint creator has no account") diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index a2039e4202..370a42d3b8 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -17,6 +17,7 @@ import ( "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/userfavorites/userfavoritesendpoints" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces/config" ) const dbReferenceError = "Unable to establish a database reference: '%v'" @@ -139,7 +140,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk ) } - if p.GetConfig().EnableUserEndpoints { + if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled { // get all endpoints determined by the APIEndpoint duplicateEndpoints, err := p.listCNSIByAPIEndpoint(apiEndpoint) if err != nil { @@ -179,7 +180,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk h := sha1.New() - if p.GetConfig().EnableUserEndpoints && !isAdmin { + if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled && !isAdmin { h.Write([]byte(apiEndpointURL.String() + userId)) } else { h.Write([]byte(apiEndpointURL.String())) @@ -213,7 +214,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk // don't save the creator if it's an admin to not break legacy features like GetCNSIRecordByEndpoint // once they have been updated, it's safe to save admin ids - if p.GetConfig().EnableUserEndpoints && !isAdmin { + if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled && !isAdmin { newCNSI.Creator = userId } @@ -276,7 +277,7 @@ func (p *portalProxy) doUnregisterCluster(cnsiGUID string) error { func (p *portalProxy) buildCNSIList(c echo.Context) ([]*interfaces.CNSIRecord, error) { log.Debug("buildCNSIList") - if p.GetConfig().EnableUserEndpoints == true { + if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled { userID, err := p.GetSessionValue(c, "user_id") if err != nil { return nil, err @@ -287,12 +288,15 @@ func (p *portalProxy) buildCNSIList(c echo.Context) ([]*interfaces.CNSIRecord, e return nil, err } - if u.Admin == false { - return p.ListAdminEndpoints(userID.(string)) + if u.Admin { + return p.ListEndpoints() + } else { + if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.AdminOnly { + return p.ListAdminEndpoints(userID.(string)) + } } } - - return p.ListEndpoints() + return p.ListAdminEndpoints("") } func (p *portalProxy) ListEndpoints() ([]*interfaces.CNSIRecord, error) { @@ -343,7 +347,9 @@ func (p *portalProxy) ListAdminEndpoints(userID string) ([]*interfaces.CNSIRecor } } adminList = append(adminList, userID) - adminList = append(adminList, "") //legacy endpoints dont have a creator + if len(userID) != 0 { + adminList = append(adminList, "") // admin endpoint without saved id + } //get a cnsi list from every admin found and given userID cnsiRepo, err := p.GetStoreFactory().EndpointStore() diff --git a/src/jetstream/info.go b/src/jetstream/info.go index afc3833a40..a1d345fc74 100644 --- a/src/jetstream/info.go +++ b/src/jetstream/info.go @@ -62,7 +62,7 @@ func (p *portalProxy) getInfo(c echo.Context) (*interfaces.Info, error) { s.Configuration.ListAllowLoadMaxed = p.Config.UIListAllowLoadMaxed s.Configuration.APIKeysEnabled = string(p.Config.APIKeysEnabled) s.Configuration.HomeViewShowFavoritesOnly = p.Config.HomeViewShowFavoritesOnly - s.Configuration.EnableUserEndpoints = p.Config.EnableUserEndpoints + s.Configuration.UserEndpointsEnabled = string(p.Config.UserEndpointsEnabled) // 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 60f7f766cc..70181ef9fb 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -1112,7 +1112,7 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, needSetupMiddleware bool) { // API endpoints with Swagger documentation and accessible with an API key that require admin permissions stableAdminAPIGroup := stableAPIGroup - if p.GetConfig().EnableUserEndpoints == true { + if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled { stableAdminAPIGroup.Use(p.endpointMiddleware) } else { stableAdminAPIGroup.Use(p.adminMiddleware) diff --git a/src/jetstream/repository/interfaces/config/config.go b/src/jetstream/repository/interfaces/config/config.go index a820481302..4d8fb46fb1 100644 --- a/src/jetstream/repository/interfaces/config/config.go +++ b/src/jetstream/repository/interfaces/config/config.go @@ -53,6 +53,39 @@ func parseAPIKeysConfigValue(input string) (APIKeysConfigValue, error) { return "", fmt.Errorf("Invalid value %q, allowed values: %q", input, allowedValues) } +// UserEndpointsConfigValue - special type for configuring whether user endpoints feature is enabled +type UserEndpointsConfigValue string + +// UserEndpointsConfigEnum - defines possible configuration values for Stratos user endpoints feature +var UserEndpointsConfigEnum = struct { + Disabled UserEndpointsConfigValue + AdminOnly UserEndpointsConfigValue + Enabled UserEndpointsConfigValue +}{ + Disabled: "disabled", + AdminOnly: "admin_only", + Enabled: "enabled", +} + +// verifies that given string is a valid config value +func parseUserEndpointsConfigValue(input string) (UserEndpointsConfigValue, error) { + t := reflect.TypeOf(UserEndpointsConfigEnum) + v := reflect.ValueOf(UserEndpointsConfigEnum) + + var allowedValues []string + + for i := 0; i < t.NumField(); i++ { + allowedValue := string(v.Field(i).Interface().(UserEndpointsConfigValue)) + if allowedValue == input { + return UserEndpointsConfigValue(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 @@ -156,8 +189,11 @@ func SetStructFieldValue(value reflect.Value, field reflect.Value, val string) e newVal = b case reflect.String: apiKeysConfigType := reflect.TypeOf((*APIKeysConfigValue)(nil)).Elem() + userEndpointsConfigType := reflect.TypeOf((*UserEndpointsConfigValue)(nil)).Elem() if typ == apiKeysConfigType { newVal, err = parseAPIKeysConfigValue(val) + } else if typ == userEndpointsConfigType { + newVal, err = parseUserEndpointsConfigValue(val) } else { newVal = val } diff --git a/src/jetstream/repository/interfaces/structs.go b/src/jetstream/repository/interfaces/structs.go index bd636d8d5a..b3dc6df82c 100644 --- a/src/jetstream/repository/interfaces/structs.go +++ b/src/jetstream/repository/interfaces/structs.go @@ -241,7 +241,7 @@ type Info struct { ListAllowLoadMaxed bool `json:"listAllowLoadMaxed,omitempty"` APIKeysEnabled string `json:"APIKeysEnabled"` HomeViewShowFavoritesOnly bool `json:"homeViewShowFavoritesOnly"` - EnableUserEndpoints bool `json:"enableUserEndpoints"` + UserEndpointsEnabled string `json:"UserEndpointsEnabled"` } `json:"config"` } @@ -402,9 +402,9 @@ type PortalConfig struct { DatabaseProviderName string EnableTechPreview bool `configName:"ENABLE_TECH_PREVIEW"` CanMigrateDatabaseSchema bool - APIKeysEnabled config.APIKeysConfigValue `configName:"API_KEYS_ENABLED"` - HomeViewShowFavoritesOnly bool `configName:"HOME_VIEW_SHOW_FAVORITES_ONLY"` - EnableUserEndpoints bool `configName:"ENABLE_USER_ENDPOINTS"` + APIKeysEnabled config.APIKeysConfigValue `configName:"API_KEYS_ENABLED"` + HomeViewShowFavoritesOnly bool `configName:"HOME_VIEW_SHOW_FAVORITES_ONLY"` + UserEndpointsEnabled config.UserEndpointsConfigValue `configName:"USER_ENDPOINTS_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 fa41bd3a07121ea09d1e98fe968fe6b81ca45df2 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 11 Feb 2021 16:57:47 +0100 Subject: [PATCH 026/101] Adjust frontend to new endpoints env var enum (#4753) Signed-off-by: Thomas Quandt --- .../create-endpoint-cf-step-1.component.ts | 2 +- .../endpoint/endpoint-card/endpoint-card.component.ts | 2 +- .../list/list-types/endpoint/endpoint-list.helpers.ts | 6 +++--- .../core/src/shared/services/session.service.ts | 10 +++++++++- .../git-registration/git-registration.component.ts | 2 +- src/frontend/packages/store/src/types/auth.types.ts | 7 ++++++- 6 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts index e9e64731d8..ac3ecf97c4 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts @@ -98,7 +98,7 @@ export class CreateEndpointCfStep1Component implements IStepperStep, AfterConten })) ); - this.overwritePermission = this.sessionService.userEndpointsEnabled().pipe( + this.overwritePermission = this.sessionService.userEndpointsNotDisabled().pipe( map(enabled => enabled ? [StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT] : []) ); diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.ts index 0451257ffa..4849f8a167 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.ts @@ -133,7 +133,7 @@ export class EndpointCardComponent extends CardCell implements On this.favorite = this.userFavoriteManager.getFavoriteEndpointFromEntity(this.row); const e = this.endpointCatalogEntity.definition; this.hasDetails = !!e && !!e.listDetailsComponent; - this.enableUserEndpoints$ = this.sessionService.userEndpointsEnabled(); + this.enableUserEndpoints$ = this.sessionService.userEndpointsNotDisabled(); } ngOnDestroy(): void { diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts index 1b1cdccf2f..4d6530ab2f 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts @@ -137,7 +137,7 @@ export class EndpointListHelper { description: '', createVisible: (row$: Observable) => { return combineLatest([ - this.sessionService.userEndpointsEnabled(), + this.sessionService.userEndpointsNotDisabled(), this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT), row$ ]).pipe( @@ -172,7 +172,7 @@ export class EndpointListHelper { description: 'Remove the endpoint', createVisible: (row$: Observable) => { return combineLatest([ - this.sessionService.userEndpointsEnabled(), + this.sessionService.userEndpointsNotDisabled(), this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT), this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT), row$ @@ -196,7 +196,7 @@ export class EndpointListHelper { description: 'Edit the endpoint', createVisible: (row$: Observable) => { return combineLatest([ - this.sessionService.userEndpointsEnabled(), + this.sessionService.userEndpointsNotDisabled(), this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT), this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT), row$ diff --git a/src/frontend/packages/core/src/shared/services/session.service.ts b/src/frontend/packages/core/src/shared/services/session.service.ts index 4253145203..4341d5481a 100644 --- a/src/frontend/packages/core/src/shared/services/session.service.ts +++ b/src/frontend/packages/core/src/shared/services/session.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; +import { UserEndpointsEnabled } from 'frontend/packages/store/src/types/auth.types'; import { Observable } from 'rxjs'; import { first, map } from 'rxjs/operators'; @@ -21,7 +22,14 @@ export class SessionService { userEndpointsEnabled(): Observable { return this.store.select(selectSessionData()).pipe( first(), - map(sessionData => sessionData.config.enableUserEndpoints || false) + map(sessionData => sessionData.config.UserEndpointsEnabled === UserEndpointsEnabled.ENABLED ? true : false) + ); + } + + userEndpointsNotDisabled(): Observable { + return this.store.select(selectSessionData()).pipe( + first(), + map(sessionData => sessionData.config.UserEndpointsEnabled !== UserEndpointsEnabled.DISABLED ? true : false) ); } } diff --git a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts index 5e52efb199..4ac2ff8f8b 100644 --- a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts +++ b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts @@ -157,7 +157,7 @@ export class GitRegistrationComponent implements OnDestroy { })) ); - this.overwritePermission = this.sessionService.userEndpointsEnabled().pipe( + this.overwritePermission = this.sessionService.userEndpointsNotDisabled().pipe( map(enabled => enabled ? [StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT] : []) ); diff --git a/src/frontend/packages/store/src/types/auth.types.ts b/src/frontend/packages/store/src/types/auth.types.ts index 2f4a2f13db..7917ae0065 100644 --- a/src/frontend/packages/store/src/types/auth.types.ts +++ b/src/frontend/packages/store/src/types/auth.types.ts @@ -27,6 +27,11 @@ export enum APIKeysEnabled { ADMIN_ONLY = 'admin_only', ALL_USERS = 'all_users' } +export enum UserEndpointsEnabled { + DISABLED = 'disabled', + ADMIN_ONLY = 'admin_only', + ENABLED = 'enabled' +} export interface SessionDataConfig { enableTechPreview?: boolean; listMaxSize?: number; @@ -34,7 +39,7 @@ export interface SessionDataConfig { APIKeysEnabled?: APIKeysEnabled; // Default value for Home View - show only favorited endpoints? homeViewShowFavoritesOnly?: boolean; - enableUserEndpoints?: boolean; + UserEndpointsEnabled?: UserEndpointsEnabled; } export interface SessionData { endpoints?: SessionEndpoints; From f1c746ab59c451676008fd67d69e75fbd89ff523 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Fri, 12 Feb 2021 14:53:25 +0100 Subject: [PATCH 027/101] Adjust mocks for tests (#4753) Signed-off-by: Thomas Quandt --- src/jetstream/auth_test.go | 6 +++--- src/jetstream/cnsi_test.go | 12 +++++++++++- src/jetstream/mock_server_test.go | 8 ++++---- src/jetstream/passthrough_test.go | 4 ++-- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/jetstream/auth_test.go b/src/jetstream/auth_test.go index 75a3d6aff0..f57656dbd9 100644 --- a/src/jetstream/auth_test.go +++ b/src/jetstream/auth_test.go @@ -369,8 +369,8 @@ func TestLoginToCNSI(t *testing.T) { DopplerLoggingEndpoint: mockDopplerEndpoint, } - expectedCNSIRow := sqlmock.NewRows([]string{"guid", "name", "cnsi_type", "api_endpoint", "auth_endpoint", "token_endpoint", "doppler_logging_endpoint", "skip_ssl_validation", "client_id", "client_secret", "allow_sso", "sub_type", "meta_data"}). - AddRow(mockCNSIGUID, mockCNSI.Name, stringCFType, mockUAA.URL, mockCNSI.AuthorizationEndpoint, mockCNSI.TokenEndpoint, mockCNSI.DopplerLoggingEndpoint, true, mockCNSI.ClientId, cipherClientSecret, true, "", "") + expectedCNSIRow := sqlmock.NewRows([]string{"guid", "name", "cnsi_type", "api_endpoint", "auth_endpoint", "token_endpoint", "doppler_logging_endpoint", "skip_ssl_validation", "client_id", "client_secret", "allow_sso", "sub_type", "meta_data", ""}). + AddRow(mockCNSIGUID, mockCNSI.Name, stringCFType, mockUAA.URL, mockCNSI.AuthorizationEndpoint, mockCNSI.TokenEndpoint, mockCNSI.DopplerLoggingEndpoint, true, mockCNSI.ClientId, cipherClientSecret, true, "", "", "") mock.ExpectQuery(selectAnyFromCNSIs). WithArgs(mockCNSIGUID). @@ -785,7 +785,7 @@ func TestVerifySession(t *testing.T) { 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 = `{"status":"ok","error":"","data":{"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","homeViewShowFavoritesOnly":false}}}` + var expectedBody = `{"status":"ok","error":"","data":{"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","homeViewShowFavoritesOnly":false,"UserEndpointsEnabled":"disabled"}}}` Convey("Should contain expected body", func() { So(res, ShouldNotBeNil) diff --git a/src/jetstream/cnsi_test.go b/src/jetstream/cnsi_test.go index 75d06549d2..4db23cad49 100644 --- a/src/jetstream/cnsi_test.go +++ b/src/jetstream/cnsi_test.go @@ -4,6 +4,7 @@ import ( "errors" "net/http" "testing" + "time" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" _ "github.com/satori/go.uuid" @@ -32,8 +33,17 @@ func TestRegisterCFCluster(t *testing.T) { _, _, ctx, pp, db, mock := setupHTTPTest(req) defer db.Close() + // Set a dummy userid in session - normally the login to UAA would do this. + sessionValues := make(map[string]interface{}) + sessionValues["user_id"] = mockUserGUID + sessionValues["exp"] = time.Now().AddDate(0, 0, 1).Unix() + + if errSession := pp.setSessionValues(ctx, sessionValues); errSession != nil { + t.Error(errors.New("unable to mock/stub user in session object")) + } + mock.ExpectExec(insertIntoCNSIs). - WithArgs(sqlmock.AnyArg(), "Some fancy CF Cluster", "cf", mockV2Info.URL, mockAuthEndpoint, mockTokenEndpoint, mockDopplerEndpoint, true, mockClientId, sqlmock.AnyArg(), false, "", ""). + WithArgs(sqlmock.AnyArg(), "Some fancy CF Cluster", "cf", mockV2Info.URL, mockAuthEndpoint, mockTokenEndpoint, mockDopplerEndpoint, true, mockClientId, sqlmock.AnyArg(), false, "", "", ""). WillReturnResult(sqlmock.NewResult(1, 1)) if err := pp.RegisterEndpoint(ctx, getCFPlugin(pp, "cf").Info); err != nil { diff --git a/src/jetstream/mock_server_test.go b/src/jetstream/mock_server_test.go index 2dbd0441d2..cee40ec292 100644 --- a/src/jetstream/mock_server_test.go +++ b/src/jetstream/mock_server_test.go @@ -176,17 +176,17 @@ func expectOneRow() sqlmock.Rows { func expectCFRow() sqlmock.Rows { return sqlmock.NewRows(rowFieldsForCNSI). - AddRow(mockCFGUID, "Some fancy CF Cluster", "cf", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, mockDopplerEndpoint, true, mockClientId, cipherClientSecret, true, "", "") + AddRow(mockCFGUID, "Some fancy CF Cluster", "cf", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, mockDopplerEndpoint, true, mockClientId, cipherClientSecret, true, "", "", "") } func expectCERow() sqlmock.Rows { return sqlmock.NewRows(rowFieldsForCNSI). - AddRow(mockCEGUID, "Some fancy HCE Cluster", "hce", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, "", true, mockClientId, cipherClientSecret, true, "", "") + AddRow(mockCEGUID, "Some fancy HCE Cluster", "hce", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, "", true, mockClientId, cipherClientSecret, true, "", "", "") } func expectCFAndCERows() sqlmock.Rows { return sqlmock.NewRows(rowFieldsForCNSI). - AddRow(mockCFGUID, "Some fancy CF Cluster", "cf", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, mockDopplerEndpoint, true, mockClientId, cipherClientSecret, false, "", ""). + AddRow(mockCFGUID, "Some fancy CF Cluster", "cf", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, mockDopplerEndpoint, true, mockClientId, cipherClientSecret, false, "", "", ""). AddRow(mockCEGUID, "Some fancy HCE Cluster", "hce", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, "", true, mockClientId, cipherClientSecret, false, "", "") } @@ -300,7 +300,7 @@ const ( getDbVersion = `SELECT version_id FROM goose_db_version WHERE is_applied = '1' ORDER BY id DESC LIMIT 1` ) -var rowFieldsForCNSI = []string{"guid", "name", "cnsi_type", "api_endpoint", "auth_endpoint", "token_endpoint", "doppler_logging_endpoint", "skip_ssl_validation", "client_id", "client_secret", "allow_sso", "sub_type", "meta_data"} +var rowFieldsForCNSI = []string{"guid", "name", "cnsi_type", "api_endpoint", "auth_endpoint", "token_endpoint", "doppler_logging_endpoint", "skip_ssl_validation", "client_id", "client_secret", "allow_sso", "sub_type", "meta_data", "creator"} var mockEncryptionKey = make([]byte, 32) diff --git a/src/jetstream/passthrough_test.go b/src/jetstream/passthrough_test.go index 5aa2f2e756..ddfaf48f8d 100644 --- a/src/jetstream/passthrough_test.go +++ b/src/jetstream/passthrough_test.go @@ -272,8 +272,8 @@ func TestValidateCNSIListWithValidGUID(t *testing.T) { _, _, _, pp, db, mock := setupHTTPTest(req) defer db.Close() - expectedCNSIRecordRow := sqlmock.NewRows([]string{"guid", "name", "cnsi_type", "api_endpoint", "auth_endpoint", "token_endpoint", "doppler_logging_endpoint", "skip_ssl_validation", "client_id", "client_secret", "allow_sso", "sub_type", "meta_data"}). - AddRow("valid-guid-abc123", "mock-name", "cf", "http://localhost", "http://localhost", "http://localhost", mockDopplerEndpoint, true, mockClientId, cipherClientSecret, true, "", "") + expectedCNSIRecordRow := sqlmock.NewRows([]string{"guid", "name", "cnsi_type", "api_endpoint", "auth_endpoint", "token_endpoint", "doppler_logging_endpoint", "skip_ssl_validation", "client_id", "client_secret", "allow_sso", "sub_type", "meta_data", ""}). + AddRow("valid-guid-abc123", "mock-name", "cf", "http://localhost", "http://localhost", "http://localhost", mockDopplerEndpoint, true, mockClientId, cipherClientSecret, true, "", "", "") mock.ExpectQuery(selectAnyFromCNSIs). WithArgs("valid-guid-abc123"). WillReturnRows(expectedCNSIRecordRow) From 32ae343c85e91e1746cc1dda42b7037023969d8d Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Fri, 12 Feb 2021 14:55:30 +0100 Subject: [PATCH 028/101] Userendpoints config default value (#4753) Signed-off-by: Thomas Quandt --- src/jetstream/config.dev | 5 +++-- src/jetstream/main.go | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/jetstream/config.dev b/src/jetstream/config.dev index e5a9a90380..bfbd708bd9 100644 --- a/src/jetstream/config.dev +++ b/src/jetstream/config.dev @@ -57,8 +57,9 @@ LOCAL_USER=admin LOCAL_USER_PASSWORD=admin LOCAL_USER_SCOPE=stratos.admin -# Enable users with a stratos.endpointadmin role to create user endpoints -ENABLE_USER_ENDPOINTS=false +# Enable users create user endpoints (disabled, admin_only, enabled). Default is disabled +# admin_only will enable admins to see created user endpoints, but users won't be able to create user endpoints or see them +USER_ENDPOINTS_ENABLED=disabled # Enable/disable API key-based access to Stratos API (disabled, admin_only, all_users). Default is admin_only #API_KEYS_ENABLED=admin_only diff --git a/src/jetstream/main.go b/src/jetstream/main.go index 70181ef9fb..15d16803a0 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -693,6 +693,12 @@ func newPortalProxy(pc interfaces.PortalConfig, dcp *sql.DB, ss HttpSessionStore pc.APIKeysEnabled = config.APIKeysConfigEnum.AdminOnly } + // Setting default value for UserEndpointsEnabled + if pc.UserEndpointsEnabled == "" { + log.Debug(`UserEndpointsEnabled not set, setting to "disabled"`) + pc.UserEndpointsEnabled = config.UserEndpointsConfigEnum.Disabled + } + pp := &portalProxy{ Config: pc, DatabaseConnectionPool: dcp, From 76174b21000e847ef82dea350c63cb6d333a660d Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Mon, 15 Feb 2021 16:01:07 +0100 Subject: [PATCH 029/101] Adjust backend-tests and fix code (#4753) * Adjust mock data * Add basic db functions to desktop plugins * Check user in cnsi login only if userendpoints enabled * Check user in cnsi register only if userendpoints enabled Signed-off-by: Thomas Quandt --- src/jetstream/auth_test.go | 10 ++++++ src/jetstream/authcnsi.go | 15 +++++--- src/jetstream/cnsi.go | 6 ++-- src/jetstream/mock_server_test.go | 2 +- src/jetstream/oauth_requests_test.go | 8 ++--- src/jetstream/plugins/desktop/endpoints.go | 8 +++++ .../plugins/desktop/kubernetes/endpoints.go | 8 +++++ .../plugins/desktop/kubernetes/tokens.go | 4 +++ src/jetstream/plugins/desktop/tokens.go | 4 +++ .../repository/cnsis/pgsql_cnsis_test.go | 36 +++++++++---------- 10 files changed, 69 insertions(+), 32 deletions(-) diff --git a/src/jetstream/auth_test.go b/src/jetstream/auth_test.go index f57656dbd9..3cb873a347 100644 --- a/src/jetstream/auth_test.go +++ b/src/jetstream/auth_test.go @@ -385,6 +385,16 @@ func TestLoginToCNSI(t *testing.T) { t.Error(errors.New("unable to mock/stub user in session object")) } + //Init the auth service + err := pp.InitStratosAuthService(interfaces.AuthEndpointTypes[pp.Config.ConsoleConfig.AuthEndpointType]) + if err != nil { + log.Warnf("%v, defaulting to auth type: remote", err) + err = pp.InitStratosAuthService(interfaces.Remote) + if err != nil { + log.Fatalf("Could not initialise auth service: %v", err) + } + } + mock.ExpectQuery(selectAnyFromTokens). WithArgs(mockCNSIGUID, mockUserGUID). WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow("0")) diff --git a/src/jetstream/authcnsi.go b/src/jetstream/authcnsi.go index 7f7e92445d..4e1bb358cb 100644 --- a/src/jetstream/authcnsi.go +++ b/src/jetstream/authcnsi.go @@ -150,13 +150,13 @@ func (p *portalProxy) DoLoginToCNSI(c echo.Context, cnsiGUID string, systemShare return nil, echo.NewHTTPError(http.StatusUnauthorized, "Could not find correct session value") } - user, err := p.StratosAuthService.GetUser(userID) - if err != nil { - return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - could not check user") - } - // admins are note allowed to connect to user created endpoints if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled && len(cnsiRecord.Creator) > 0 { + user, err := p.StratosAuthService.GetUser(userID) + if err != nil { + return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - could not check user") + } + cnsiUser, err := p.StratosAuthService.GetUser(cnsiRecord.Creator) if err != nil { return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - endpoint creator has no account") @@ -169,6 +169,11 @@ func (p *portalProxy) DoLoginToCNSI(c echo.Context, cnsiGUID string, systemShare // Register as a system endpoint? if systemSharedToken { + user, err := p.StratosAuthService.GetUser(userID) + if err != nil { + return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - could not check user") + } + // User needs to be an admin if !user.Admin { return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect System Shared endpoint - user is not an administrator") diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 370a42d3b8..e7af390b2d 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -113,12 +113,10 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk "Failed to get API Endpoint: %v", err) } - isAdmin := false + isAdmin := true // anonymous admin? - if len(userId) == 0 { - isAdmin = true - } else { + if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled && len(userId) != 0 { currentCreator, err := p.StratosAuthService.GetUser(userId) if err != nil { return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( diff --git a/src/jetstream/mock_server_test.go b/src/jetstream/mock_server_test.go index cee40ec292..d6925c26c6 100644 --- a/src/jetstream/mock_server_test.go +++ b/src/jetstream/mock_server_test.go @@ -187,7 +187,7 @@ func expectCERow() sqlmock.Rows { func expectCFAndCERows() sqlmock.Rows { return sqlmock.NewRows(rowFieldsForCNSI). AddRow(mockCFGUID, "Some fancy CF Cluster", "cf", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, mockDopplerEndpoint, true, mockClientId, cipherClientSecret, false, "", "", ""). - AddRow(mockCEGUID, "Some fancy HCE Cluster", "hce", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, "", true, mockClientId, cipherClientSecret, false, "", "") + AddRow(mockCEGUID, "Some fancy HCE Cluster", "hce", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, "", true, mockClientId, cipherClientSecret, false, "", "", "") } func expectTokenRow() sqlmock.Rows { diff --git a/src/jetstream/oauth_requests_test.go b/src/jetstream/oauth_requests_test.go index 76c8de6a1f..fe8e8c248a 100644 --- a/src/jetstream/oauth_requests_test.go +++ b/src/jetstream/oauth_requests_test.go @@ -107,8 +107,8 @@ func TestDoOauthFlowRequestWithValidToken(t *testing.T) { // p.GetCNSIRecord(r.GUID) -> cnsiRepo.Find(guid) - expectedCNSIRecordRow := sqlmock.NewRows([]string{"guid", "name", "cnsi_type", "api_endpoint", "auth_endpoint", "token_endpoint", "doppler_logging_endpoint", "skip_ssl_validation", "client_id", "client_secret", "allow_sso", "sub_type", "meta_data"}). - AddRow(mockCNSI.GUID, mockCNSI.Name, mockCNSI.CNSIType, mockURLasString, mockCNSI.AuthorizationEndpoint, mockCNSI.TokenEndpoint, mockCNSI.DopplerLoggingEndpoint, true, mockCNSI.ClientId, cipherClientSecret, true, "", "") + expectedCNSIRecordRow := sqlmock.NewRows([]string{"guid", "name", "cnsi_type", "api_endpoint", "auth_endpoint", "token_endpoint", "doppler_logging_endpoint", "skip_ssl_validation", "client_id", "client_secret", "allow_sso", "sub_type", "meta_data", "creator"}). + AddRow(mockCNSI.GUID, mockCNSI.Name, mockCNSI.CNSIType, mockURLasString, mockCNSI.AuthorizationEndpoint, mockCNSI.TokenEndpoint, mockCNSI.DopplerLoggingEndpoint, true, mockCNSI.ClientId, cipherClientSecret, true, "", "", "") mock.ExpectQuery(selectAnyFromCNSIs). WithArgs(mockCNSIGUID). WillReturnRows(expectedCNSIRecordRow) @@ -238,8 +238,8 @@ func TestDoOauthFlowRequestWithExpiredToken(t *testing.T) { WillReturnRows(expectedCNSITokenRow) // p.GetCNSIRecord(r.GUID) -> cnsiRepo.Find(guid) - expectedCNSIRecordRow := sqlmock.NewRows([]string{"guid", "name", "cnsi_type", "api_endpoint", "auth_endpoint", "token_endpoint", "doppler_logging_endpoint", "skip_ssl_validation", "client_id", "client_secret", "allow_sso", "sub_type", "meta_data"}). - AddRow(mockCNSI.GUID, mockCNSI.Name, mockCNSI.CNSIType, mockURLasString, mockCNSI.AuthorizationEndpoint, mockCNSI.TokenEndpoint, mockCNSI.DopplerLoggingEndpoint, true, mockCNSI.ClientId, cipherClientSecret, true, "", "") + expectedCNSIRecordRow := sqlmock.NewRows([]string{"guid", "name", "cnsi_type", "api_endpoint", "auth_endpoint", "token_endpoint", "doppler_logging_endpoint", "skip_ssl_validation", "client_id", "client_secret", "allow_sso", "sub_type", "meta_data", "creator"}). + AddRow(mockCNSI.GUID, mockCNSI.Name, mockCNSI.CNSIType, mockURLasString, mockCNSI.AuthorizationEndpoint, mockCNSI.TokenEndpoint, mockCNSI.DopplerLoggingEndpoint, true, mockCNSI.ClientId, cipherClientSecret, true, "", "", "") mock.ExpectQuery(selectAnyFromCNSIs). WithArgs(mockCNSIGUID). WillReturnRows(expectedCNSIRecordRow) diff --git a/src/jetstream/plugins/desktop/endpoints.go b/src/jetstream/plugins/desktop/endpoints.go index 5314f2acd5..4de98370f1 100644 --- a/src/jetstream/plugins/desktop/endpoints.go +++ b/src/jetstream/plugins/desktop/endpoints.go @@ -41,6 +41,14 @@ func (d *DesktopEndpointStore) FindByAPIEndpoint(endpoint string, encryptionKey return d.store.FindByAPIEndpoint(endpoint, encryptionKey) } +func (d *DesktopEndpointStore) ListByAPIEndpoint(endpoint string, encryptionKey []byte) ([]*interfaces.CNSIRecord, error) { + return d.store.ListByAPIEndpoint(endpoint, encryptionKey) +} + +func (d *DesktopEndpointStore) ListByCreator(userGUID string, encryptionKey []byte) ([]*interfaces.CNSIRecord, error) { + return d.store.ListByCreator(userGUID, encryptionKey) +} + func (d *DesktopEndpointStore) Delete(guid string) error { if IsLocalCloudFoundry(guid) { updates := make(map[string]string) diff --git a/src/jetstream/plugins/desktop/kubernetes/endpoints.go b/src/jetstream/plugins/desktop/kubernetes/endpoints.go index 1539c8f43c..5f01a52cfc 100644 --- a/src/jetstream/plugins/desktop/kubernetes/endpoints.go +++ b/src/jetstream/plugins/desktop/kubernetes/endpoints.go @@ -46,6 +46,14 @@ func (d *EndpointStore) FindByAPIEndpoint(endpoint string, encryptionKey []byte) return d.store.FindByAPIEndpoint(endpoint, encryptionKey) } +func (d *EndpointStore) ListByAPIEndpoint(endpoint string, encryptionKey []byte) ([]*interfaces.CNSIRecord, error) { + return d.store.ListByAPIEndpoint(endpoint, encryptionKey) +} + +func (d *EndpointStore) ListByCreator(userGUID string, encryptionKey []byte) ([]*interfaces.CNSIRecord, error) { + return d.store.ListByCreator(userGUID, encryptionKey) +} + func (d *EndpointStore) Delete(guid string) error { return d.store.Delete(guid) } diff --git a/src/jetstream/plugins/desktop/kubernetes/tokens.go b/src/jetstream/plugins/desktop/kubernetes/tokens.go index c2df81029a..eef50d3cfe 100644 --- a/src/jetstream/plugins/desktop/kubernetes/tokens.go +++ b/src/jetstream/plugins/desktop/kubernetes/tokens.go @@ -14,6 +14,10 @@ func (d *TokenStore) FindAuthToken(userGUID string, encryptionKey []byte) (inter return d.store.FindAuthToken(userGUID, encryptionKey) } +func (d *TokenStore) ListAuthToken(encryptionKey []byte) ([]interfaces.TokenRecord, error) { + return d.store.ListAuthToken(encryptionKey) +} + func (d *TokenStore) SaveAuthToken(userGUID string, tokenRecord interfaces.TokenRecord, encryptionKey []byte) error { return d.store.SaveAuthToken(userGUID, tokenRecord, encryptionKey) } diff --git a/src/jetstream/plugins/desktop/tokens.go b/src/jetstream/plugins/desktop/tokens.go index 5063b912d1..6c528e6af9 100644 --- a/src/jetstream/plugins/desktop/tokens.go +++ b/src/jetstream/plugins/desktop/tokens.go @@ -17,6 +17,10 @@ func (d *TokenStore) FindAuthToken(userGUID string, encryptionKey []byte) (inter return d.store.FindAuthToken(userGUID, encryptionKey) } +func (d *TokenStore) ListAuthToken(encryptionKey []byte) ([]interfaces.TokenRecord, error) { + return d.store.ListAuthToken(encryptionKey) +} + func (d *TokenStore) SaveAuthToken(userGUID string, tokenRecord interfaces.TokenRecord, encryptionKey []byte) error { return d.store.SaveAuthToken(userGUID, tokenRecord, encryptionKey) } diff --git a/src/jetstream/repository/cnsis/pgsql_cnsis_test.go b/src/jetstream/repository/cnsis/pgsql_cnsis_test.go index 1d66395e19..3ae432aca8 100644 --- a/src/jetstream/repository/cnsis/pgsql_cnsis_test.go +++ b/src/jetstream/repository/cnsis/pgsql_cnsis_test.go @@ -32,7 +32,7 @@ func TestPgSQLCNSIs(t *testing.T) { insertIntoCNSIs = `INSERT INTO cnsis` deleteFromCNSIs = `DELETE FROM cnsis WHERE (.+)` rowFieldsForCNSI = []string{"guid", "name", "cnsi_type", "api_endpoint", "auth_endpoint", - "token_endpoint", "doppler_logging_endpoint", "skip_ssl_validation", "client_id", "client_secret", "sso_allowed", "sub_type", "meta_data"} + "token_endpoint", "doppler_logging_endpoint", "skip_ssl_validation", "client_id", "client_secret", "sso_allowed", "sub_type", "meta_data", "creator"} mockEncryptionKey = make([]byte, 32) ) cipherClientSecret, _ := crypto.EncryptToken(mockEncryptionKey, mockClientSecret) @@ -111,13 +111,13 @@ func TestPgSQLCNSIs(t *testing.T) { // general setup u, _ := url.Parse(mockAPIEndpoint) - r1 := &interfaces.CNSIRecord{GUID: mockCFGUID, Name: "Some fancy CF Cluster", CNSIType: "cf", APIEndpoint: u, AuthorizationEndpoint: mockAuthEndpoint, TokenEndpoint: mockAuthEndpoint, DopplerLoggingEndpoint: mockDopplerEndpoint, SkipSSLValidation: true, ClientId: mockClientId, ClientSecret: mockClientSecret, SSOAllowed: false} - r2 := &interfaces.CNSIRecord{GUID: mockCEGUID, Name: "Some fancy HCE Cluster", CNSIType: "hce", APIEndpoint: u, AuthorizationEndpoint: mockAuthEndpoint, TokenEndpoint: mockAuthEndpoint, DopplerLoggingEndpoint: "", SkipSSLValidation: true, ClientId: mockClientId, ClientSecret: mockClientSecret, SSOAllowed: false} + r1 := &interfaces.CNSIRecord{GUID: mockCFGUID, Name: "Some fancy CF Cluster", CNSIType: "cf", APIEndpoint: u, AuthorizationEndpoint: mockAuthEndpoint, TokenEndpoint: mockAuthEndpoint, DopplerLoggingEndpoint: mockDopplerEndpoint, SkipSSLValidation: true, ClientId: mockClientId, ClientSecret: mockClientSecret, SSOAllowed: false, Creator: ""} + r2 := &interfaces.CNSIRecord{GUID: mockCEGUID, Name: "Some fancy HCE Cluster", CNSIType: "hce", APIEndpoint: u, AuthorizationEndpoint: mockAuthEndpoint, TokenEndpoint: mockAuthEndpoint, DopplerLoggingEndpoint: "", SkipSSLValidation: true, ClientId: mockClientId, ClientSecret: mockClientSecret, SSOAllowed: false, Creator: ""} expectedList = append(expectedList, r1, r2) mockCFAndCERows = sqlmock.NewRows(rowFieldsForCNSI). - AddRow(mockCFGUID, "Some fancy CF Cluster", "cf", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, mockDopplerEndpoint, true, mockClientId, cipherClientSecret, false, "", ""). - AddRow(mockCEGUID, "Some fancy HCE Cluster", "hce", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, "", true, mockClientId, cipherClientSecret, false, "", "") + AddRow(mockCFGUID, "Some fancy CF Cluster", "cf", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, mockDopplerEndpoint, true, mockClientId, cipherClientSecret, false, "", "", ""). + AddRow(mockCEGUID, "Some fancy HCE Cluster", "hce", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, "", true, mockClientId, cipherClientSecret, false, "", "", "") mock.ExpectQuery(selectAnyFromCNSIs). WillReturnRows(mockCFAndCERows) @@ -182,7 +182,7 @@ func TestPgSQLCNSIs(t *testing.T) { var ( //SELECT c.guid, c.name, c.cnsi_type, c.api_endpoint, c.doppler_logging_endpoint, t.user_guid, t.token_expiry, c.skip_ssl_validation, t.disconnected, t.meta_data //rowFieldsForCluster = []string{"guid", "name", "cnsi_type", "api_endpoint", "account", "token_expiry", "skip_ssl_validation"} - rowFieldsForCluster = []string{"guid", "name", "cnsi_type", "api_endpoint", "doppler_logging_endpoint", "account", "token_expiry", "skip_ssl_validation", "disconnected", "meta_data", "sub_type", "endpoint_metadata"} + rowFieldsForCluster = []string{"guid", "name", "cnsi_type", "api_endpoint", "doppler_logging_endpoint", "account", "token_expiry", "skip_ssl_validation", "disconnected", "meta_data", "sub_type", "endpoint_metadata", "creator"} expectedList []*interfaces.ConnectedEndpoint mockAccount = "asd-gjfg-bob" ) @@ -239,13 +239,13 @@ func TestPgSQLCNSIs(t *testing.T) { // general setup u, _ := url.Parse(mockAPIEndpoint) - r1 := &interfaces.ConnectedEndpoint{GUID: mockCFGUID, Name: "Some fancy CF Cluster", CNSIType: "cf", APIEndpoint: u, DopplerLoggingEndpoint: mockDopplerEndpoint, Account: mockAccount, TokenExpiry: mockTokenExpiry, SkipSSLValidation: true} - r2 := &interfaces.ConnectedEndpoint{GUID: mockCEGUID, Name: "Some fancy HCE Cluster", CNSIType: "hce", APIEndpoint: u, DopplerLoggingEndpoint: mockDopplerEndpoint, Account: mockAccount, TokenExpiry: mockTokenExpiry, SkipSSLValidation: true} + r1 := &interfaces.ConnectedEndpoint{GUID: mockCFGUID, Name: "Some fancy CF Cluster", CNSIType: "cf", APIEndpoint: u, DopplerLoggingEndpoint: mockDopplerEndpoint, Account: mockAccount, TokenExpiry: mockTokenExpiry, SkipSSLValidation: true, Creator: ""} + r2 := &interfaces.ConnectedEndpoint{GUID: mockCEGUID, Name: "Some fancy HCE Cluster", CNSIType: "hce", APIEndpoint: u, DopplerLoggingEndpoint: mockDopplerEndpoint, Account: mockAccount, TokenExpiry: mockTokenExpiry, SkipSSLValidation: true, Creator: ""} expectedList = append(expectedList, r1, r2) mockClusterList = sqlmock.NewRows(rowFieldsForCluster). - AddRow(mockCFGUID, "Some fancy CF Cluster", "cf", mockAPIEndpoint, mockDopplerEndpoint, mockAccount, mockTokenExpiry, true, false, "", "", ""). - AddRow(mockCEGUID, "Some fancy HCE Cluster", "hce", mockAPIEndpoint, mockDopplerEndpoint, mockAccount, mockTokenExpiry, true, false, "", "", "") + AddRow(mockCFGUID, "Some fancy CF Cluster", "cf", mockAPIEndpoint, mockDopplerEndpoint, mockAccount, mockTokenExpiry, true, false, "", "", "", ""). + AddRow(mockCEGUID, "Some fancy HCE Cluster", "hce", mockAPIEndpoint, mockDopplerEndpoint, mockAccount, mockTokenExpiry, true, false, "", "", "", "") mock.ExpectQuery(selectFromCNSIandTokensWhere). WillReturnRows(mockClusterList) @@ -317,10 +317,10 @@ func TestPgSQLCNSIs(t *testing.T) { // General setup u, _ := url.Parse(mockAPIEndpoint) - expectedCNSIRecord := interfaces.CNSIRecord{GUID: mockCFGUID, Name: "Some fancy CF Cluster", CNSIType: "cf", APIEndpoint: u, AuthorizationEndpoint: mockAuthEndpoint, TokenEndpoint: mockAuthEndpoint, DopplerLoggingEndpoint: mockDopplerEndpoint, SkipSSLValidation: true, ClientId: mockClientId, ClientSecret: mockClientSecret, SSOAllowed: false} + expectedCNSIRecord := interfaces.CNSIRecord{GUID: mockCFGUID, Name: "Some fancy CF Cluster", CNSIType: "cf", APIEndpoint: u, AuthorizationEndpoint: mockAuthEndpoint, TokenEndpoint: mockAuthEndpoint, DopplerLoggingEndpoint: mockDopplerEndpoint, SkipSSLValidation: true, ClientId: mockClientId, ClientSecret: mockClientSecret, SSOAllowed: false, Creator: ""} rs := sqlmock.NewRows(rowFieldsForCNSI). - AddRow(mockCFGUID, "Some fancy CF Cluster", "cf", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, mockDopplerEndpoint, true, mockClientId, cipherClientSecret, false, "", "") + AddRow(mockCFGUID, "Some fancy CF Cluster", "cf", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, mockDopplerEndpoint, true, mockClientId, cipherClientSecret, false, "", "", "") mock.ExpectQuery(selectFromCNSIsWhere). WillReturnRows(rs) @@ -414,10 +414,10 @@ func TestPgSQLCNSIs(t *testing.T) { // General setup u, _ := url.Parse(mockAPIEndpoint) - expectedCNSIRecord := interfaces.CNSIRecord{GUID: mockCFGUID, Name: "Some fancy CF Cluster", CNSIType: "cf", APIEndpoint: u, AuthorizationEndpoint: mockAuthEndpoint, TokenEndpoint: mockAuthEndpoint, DopplerLoggingEndpoint: mockDopplerEndpoint, SkipSSLValidation: true, ClientId: mockClientId, ClientSecret: mockClientSecret, SSOAllowed: true} + expectedCNSIRecord := interfaces.CNSIRecord{GUID: mockCFGUID, Name: "Some fancy CF Cluster", CNSIType: "cf", APIEndpoint: u, AuthorizationEndpoint: mockAuthEndpoint, TokenEndpoint: mockAuthEndpoint, DopplerLoggingEndpoint: mockDopplerEndpoint, SkipSSLValidation: true, ClientId: mockClientId, ClientSecret: mockClientSecret, SSOAllowed: true, Creator: ""} rs := sqlmock.NewRows(rowFieldsForCNSI). - AddRow(mockCFGUID, "Some fancy CF Cluster", "cf", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, mockDopplerEndpoint, true, mockClientId, cipherClientSecret, true, "", "") + AddRow(mockCFGUID, "Some fancy CF Cluster", "cf", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, mockDopplerEndpoint, true, mockClientId, cipherClientSecret, true, "", "", "") mock.ExpectQuery(selectFromCNSIsWhere). WillReturnRows(rs) @@ -512,10 +512,10 @@ func TestPgSQLCNSIs(t *testing.T) { // General setup u, _ := url.Parse(mockAPIEndpoint) - cnsi := interfaces.CNSIRecord{GUID: mockCFGUID, Name: "Some fancy CF Cluster", CNSIType: "cf", APIEndpoint: u, AuthorizationEndpoint: mockAuthEndpoint, TokenEndpoint: mockAuthEndpoint, DopplerLoggingEndpoint: mockDopplerEndpoint, SkipSSLValidation: true, ClientId: mockClientId, ClientSecret: mockClientSecret, SSOAllowed: true} + cnsi := interfaces.CNSIRecord{GUID: mockCFGUID, Name: "Some fancy CF Cluster", CNSIType: "cf", APIEndpoint: u, AuthorizationEndpoint: mockAuthEndpoint, TokenEndpoint: mockAuthEndpoint, DopplerLoggingEndpoint: mockDopplerEndpoint, SkipSSLValidation: true, ClientId: mockClientId, ClientSecret: mockClientSecret, SSOAllowed: true, Creator: ""} mock.ExpectExec(insertIntoCNSIs). - WithArgs(mockCFGUID, "Some fancy CF Cluster", "cf", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, mockDopplerEndpoint, true, mockClientId, sqlmock.AnyArg(), true, sqlmock.AnyArg(), sqlmock.AnyArg()). + WithArgs(mockCFGUID, "Some fancy CF Cluster", "cf", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, mockDopplerEndpoint, true, mockClientId, sqlmock.AnyArg(), true, sqlmock.AnyArg(), sqlmock.AnyArg(), ""). WillReturnResult(sqlmock.NewResult(1, 1)) Convey("there should be no error returned", func() { @@ -532,11 +532,11 @@ func TestPgSQLCNSIs(t *testing.T) { // General setup u, _ := url.Parse(mockAPIEndpoint) - cnsi := interfaces.CNSIRecord{GUID: mockCFGUID, Name: "Some fancy CF Cluster", CNSIType: "cf", APIEndpoint: u, AuthorizationEndpoint: mockAuthEndpoint, TokenEndpoint: mockAuthEndpoint, DopplerLoggingEndpoint: mockDopplerEndpoint, SkipSSLValidation: true, ClientId: mockClientId, ClientSecret: mockClientSecret, SSOAllowed: true} + cnsi := interfaces.CNSIRecord{GUID: mockCFGUID, Name: "Some fancy CF Cluster", CNSIType: "cf", APIEndpoint: u, AuthorizationEndpoint: mockAuthEndpoint, TokenEndpoint: mockAuthEndpoint, DopplerLoggingEndpoint: mockDopplerEndpoint, SkipSSLValidation: true, ClientId: mockClientId, ClientSecret: mockClientSecret, SSOAllowed: true, Creator: ""} expectedErrorMessage := fmt.Sprintf("Unable to Save CNSI record: %s", unknownDBError) mock.ExpectExec(insertIntoCNSIs). - WithArgs(mockCFGUID, "Some fancy CF Cluster", "cf", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, mockDopplerEndpoint, true, mockClientId, sqlmock.AnyArg(), true, sqlmock.AnyArg(), sqlmock.AnyArg()). + WithArgs(mockCFGUID, "Some fancy CF Cluster", "cf", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, mockDopplerEndpoint, true, mockClientId, sqlmock.AnyArg(), true, sqlmock.AnyArg(), sqlmock.AnyArg(), ""). WillReturnError(errors.New(unknownDBError)) Convey("there should be an error returned", func() { From 35ebcf9e3b136addbc0c08e52d32d788f92df18a Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Tue, 16 Feb 2021 10:25:28 +0100 Subject: [PATCH 030/101] Dont check creator account for admin scope (#4753) * Only user-endpoints save an id in creator column * If user cant be found, set it as anonymous user Signed-off-by: Thomas Quandt --- src/jetstream/authcnsi.go | 9 ++------ src/jetstream/cnsi.go | 45 +++++++-------------------------------- src/jetstream/info.go | 13 ++++++----- 3 files changed, 16 insertions(+), 51 deletions(-) diff --git a/src/jetstream/authcnsi.go b/src/jetstream/authcnsi.go index 4e1bb358cb..8c2ae209df 100644 --- a/src/jetstream/authcnsi.go +++ b/src/jetstream/authcnsi.go @@ -151,18 +151,13 @@ func (p *portalProxy) DoLoginToCNSI(c echo.Context, cnsiGUID string, systemShare } // admins are note allowed to connect to user created endpoints - if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled && len(cnsiRecord.Creator) > 0 { + if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled && len(cnsiRecord.Creator) != 0 { user, err := p.StratosAuthService.GetUser(userID) if err != nil { return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - could not check user") } - cnsiUser, err := p.StratosAuthService.GetUser(cnsiRecord.Creator) - if err != nil { - return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - endpoint creator has no account") - } - - if !cnsiUser.Admin && user.Admin { + if user.Admin { return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - admins are not allowed to connect to user created endpoints") } } diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index e7af390b2d..9136ed38b5 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -151,8 +151,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk // check if we've already got this APIEndpoint in this DB for _, duplicate := range duplicateEndpoints { - creatorRecord, err := p.StratosAuthService.GetUser(duplicate.Creator) - if len(duplicate.Creator) == 0 || (err == nil && creatorRecord.Admin == true) { + if len(duplicate.Creator) == 0 { return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( http.StatusBadRequest, "Can not register same admin endpoint multiple times", @@ -177,13 +176,12 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk } h := sha1.New() - + // see why its generated this way in Issue #4753 / #3031 if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled && !isAdmin { h.Write([]byte(apiEndpointURL.String() + userId)) } else { h.Write([]byte(apiEndpointURL.String())) } - guid := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) newCNSI, _, err := fetchInfo(apiEndpoint, skipSSLValidation) @@ -210,8 +208,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk newCNSI.SSOAllowed = ssoAllowed newCNSI.SubType = subType - // don't save the creator if it's an admin to not break legacy features like GetCNSIRecordByEndpoint - // once they have been updated, it's safe to save admin ids + // admins currently can't create user endpoints if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled && !isAdmin { newCNSI.Creator = userId } @@ -320,33 +317,12 @@ func (p *portalProxy) ListAdminEndpoints(userID string) ([]*interfaces.CNSIRecor log.Debug("ListAdminEndpoints") var cnsiList []*interfaces.CNSIRecord - var adminList []string + var userList []string var err error - //look up all admin ids in tokens - tokenRepo, err := p.GetStoreFactory().TokenStore() - if err != nil { - return cnsiList, fmt.Errorf("listRegisteredTokens: %s", err) - } - tokenList, err := tokenRepo.ListAuthToken(p.Config.EncryptionKeyInBytes) - if err != nil { - return cnsiList, err - } - - //search for admins and add them to list - for _, token := range tokenList { - tokenInfo, err := p.GetUserTokenInfo(token.AuthToken) - if err != nil { - return cnsiList, err - } - stratosAdmin := strings.Contains(strings.Join(tokenInfo.Scope, ""), p.GetConfig().ConsoleConfig.ConsoleAdminScope) - if stratosAdmin == true { - adminList = append(adminList, tokenInfo.UserGUID) - } - } - adminList = append(adminList, userID) + userList = append(userList, userID) if len(userID) != 0 { - adminList = append(adminList, "") // admin endpoint without saved id + userList = append(userList, "") } //get a cnsi list from every admin found and given userID @@ -355,8 +331,8 @@ func (p *portalProxy) ListAdminEndpoints(userID string) ([]*interfaces.CNSIRecor return cnsiList, fmt.Errorf("listRegisteredCNSIs: %s", err) } - for _, adminID := range adminList { - creatorList, err := cnsiRepo.ListByCreator(adminID, p.Config.EncryptionKeyInBytes) + for _, id := range userList { + creatorList, err := cnsiRepo.ListByCreator(id, p.Config.EncryptionKeyInBytes) if err != nil { return creatorList, err } @@ -537,11 +513,6 @@ func (p *portalProxy) GetAdminCNSIRecordByEndpoint(endpoint string) (interfaces. for _, endpoint := range endpointList { if len(endpoint.Creator) == 0 { rec = endpoint - } else { - creatorRecord, err := p.StratosAuthService.GetUser(endpoint.Creator) - if err == nil && creatorRecord.Admin == true { - rec = endpoint - } } } diff --git a/src/jetstream/info.go b/src/jetstream/info.go index a1d345fc74..ea4a57b719 100644 --- a/src/jetstream/info.go +++ b/src/jetstream/info.go @@ -106,15 +106,14 @@ func (p *portalProxy) getInfo(c echo.Context) (*interfaces.Info, error) { Admin: true, } - // try to get additional creator information for this cnsi + // assume it's a user when len != 0 if len(cnsi.Creator) != 0 { + endpoint.Creator.Admin = false u, err := p.StratosAuthService.GetUser(cnsi.Creator) - if err == nil { - endpoint.Creator.Admin = u.Admin - // dont set username of admins for security reasons - if u.Admin == false { - endpoint.Creator.Name = u.Name - } + if err != nil { + endpoint.Creator.Name = "user" + } else { + endpoint.Creator.Name = u.Name } } From e9a2bd0c8885474f6dddd4f84bd6b7aac53638dc Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 18 Feb 2021 09:34:37 +0100 Subject: [PATCH 031/101] Fix Frontend Lint errors (#4753) Signed-off-by: Thomas Quandt --- .../features/cf/user-invites/user-invite.service.ts | 2 +- src/frontend/packages/core/src/core/user.service.ts | 8 ++++---- .../create-endpoint-cf-step-1.component.ts | 2 +- .../endpoints-page/endpoints-page.component.ts | 4 ++-- .../list-types/endpoint/base-endpoints-data-source.ts | 4 ++-- .../list/list-types/endpoint/endpoint-list.helpers.ts | 10 +++++----- .../core/src/shared/user-permission.directive.ts | 8 ++++---- .../git-registration/git-registration.component.ts | 5 +++-- 8 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/frontend/packages/cloud-foundry/src/features/cf/user-invites/user-invite.service.ts b/src/frontend/packages/cloud-foundry/src/features/cf/user-invites/user-invite.service.ts index e9d5fab645..ac8a10bd64 100644 --- a/src/frontend/packages/cloud-foundry/src/features/cf/user-invites/user-invite.service.ts +++ b/src/frontend/packages/cloud-foundry/src/features/cf/user-invites/user-invite.service.ts @@ -151,7 +151,7 @@ export class UserInviteService { map(([cf, auth, endpoint]) => cf.global.isAdmin && auth.sessionData['plugin-config'] && auth.sessionData['plugin-config'].userInvitationsEnabled === 'true' && - endpoint.entity.creator.admin) + endpoint.entity.creator.admin) ); } diff --git a/src/frontend/packages/core/src/core/user.service.ts b/src/frontend/packages/core/src/core/user.service.ts index 956aef5e33..20c46eeb01 100644 --- a/src/frontend/packages/core/src/core/user.service.ts +++ b/src/frontend/packages/core/src/core/user.service.ts @@ -15,12 +15,12 @@ export class UserService { constructor(store: Store) { this.isAdmin$ = store.select(s => s.auth).pipe( map((auth: AuthState) => auth.sessionData && auth.sessionData.user && auth.sessionData.user.admin)); - + this.isEndpointAdmin$ = store.select(s => s.auth).pipe( map((auth: AuthState) => { - return (auth.sessionData - && auth.sessionData.user - && auth.sessionData.user.scopes.find(e => e === "stratos.endpointadmin") !== undefined); + return (auth.sessionData + && auth.sessionData.user + && auth.sessionData.user.scopes.find(e => e === 'stratos.endpointadmin') !== undefined); })); } diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts index ac3ecf97c4..8c14f951da 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts @@ -175,7 +175,7 @@ export class CreateEndpointCfStep1Component implements IStepperStep, AfterConten toggleOverwriteEndpoints() { // wait a tick for validators to adjust to new data in the directive - setTimeout(()=>{ + setTimeout(() => { this.registerForm.controls.nameField.updateValueAndValidity(); this.registerForm.controls.urlField.updateValueAndValidity(); }); diff --git a/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts b/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts index 8604677d73..d69feadb7f 100644 --- a/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.ts @@ -91,10 +91,10 @@ export class EndpointsPageComponent implements AfterViewInit, OnDestroy, OnInit this.canRegisterEndpoint = this.sessionService.userEndpointsEnabled().pipe( map(enabled => { - if(enabled){ + if (enabled){ return [StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT, StratosCurrentUserPermissions.EDIT_ENDPOINT]; }else{ - return [StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT] + return [StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT]; } }) ); diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts index 7ed25eead8..bd1a2a8b00 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts @@ -124,8 +124,8 @@ export class BaseEndpointsDataSource extends ListDataSource { metricsAvailable: false, sso_allowed: false, creator: { - name:'', - admin:false + name: '', + admin: false } }), paginationKey: action.paginationKey, diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts index 4d6530ab2f..128667e9c1 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts @@ -142,7 +142,7 @@ export class EndpointListHelper { row$ ]).pipe( map(([userEndpointsEnabled, isAdmin, row]) => { - if(userEndpointsEnabled && !row.creator.admin && isAdmin){ + if (userEndpointsEnabled && !row.creator.admin && isAdmin){ return false; }else{ const endpoint = entityCatalog.getEndpoint(row.cnsi_type, row.sub_type); @@ -177,8 +177,8 @@ export class EndpointListHelper { this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT), row$ ]).pipe( - map(([userEndpointsEnabled, isAdmin, isEndpointAdmin, row])=>{ - if(!userEndpointsEnabled || row.creator.admin){ + map(([userEndpointsEnabled, isAdmin, isEndpointAdmin, row]) => { + if (!userEndpointsEnabled || row.creator.admin){ return isAdmin; }else{ return isEndpointAdmin || isAdmin; @@ -201,8 +201,8 @@ export class EndpointListHelper { this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT), row$ ]).pipe( - map(([userEndpointsEnabled, isAdmin, isEndpointAdmin, row])=>{ - if(!userEndpointsEnabled || row.creator.admin){ + map(([userEndpointsEnabled, isAdmin, isEndpointAdmin, row]) => { + if (!userEndpointsEnabled || row.creator.admin){ return isAdmin; }else{ return isEndpointAdmin || isAdmin; diff --git a/src/frontend/packages/core/src/shared/user-permission.directive.ts b/src/frontend/packages/core/src/shared/user-permission.directive.ts index 29d0cb5d89..2cfa77f23a 100644 --- a/src/frontend/packages/core/src/shared/user-permission.directive.ts +++ b/src/frontend/packages/core/src/shared/user-permission.directive.ts @@ -27,14 +27,14 @@ export class UserPermissionDirective implements OnDestroy, OnInit { // execute a permission check for every give permissiontype let $permissionChecks: Observable[]; $permissionChecks = this.appUserPermission.map((permission: PermissionTypes) => { - return this.currentUserPermissionsService.can(permission,this.appUserPermissionEndpointGuid) + return this.currentUserPermissionsService.can(permission, this.appUserPermissionEndpointGuid); }); // permit user if one check results true this.canSub = combineLatest($permissionChecks).pipe( - map((arr: boolean[])=>{ - for(const result of arr){ - if(result){ + map((arr: boolean[]) => { + for (const result of arr){ + if (result){ return result; } } diff --git a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts index 4ac2ff8f8b..37c8b95f32 100644 --- a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts +++ b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts @@ -226,7 +226,8 @@ export class GitRegistrationComponent implements OnDestroy { false; const overwriteEndpoints = this.registerForm.controls.overwriteEndpointsField.value; - return stratosEntityCatalog.endpoint.api.register(GIT_ENDPOINT_TYPE, this.epSubType, name, url, skipSSL, '', '', false, overwriteEndpoints) + return stratosEntityCatalog.endpoint.api.register(GIT_ENDPOINT_TYPE, + this.epSubType, name, url, skipSSL, '', '', false, overwriteEndpoints) .pipe( pairwise(), filter(([oldVal, newVal]) => (oldVal.busy && !newVal.busy)), @@ -264,7 +265,7 @@ export class GitRegistrationComponent implements OnDestroy { toggleOverwriteEndpoints() { // wait a tick for validators to adjust to new data in the directive - setTimeout(()=>{ + setTimeout(() => { this.registerForm.controls.nameField.updateValueAndValidity(); this.registerForm.controls.urlField.updateValueAndValidity(); }); From 2a08d2be6b05b7e4435c05a1a43762b7e7949b6d Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 18 Feb 2021 10:40:05 +0100 Subject: [PATCH 032/101] Split endpointMiddleware into two (#4753) Signed-off-by: Thomas Quandt --- src/jetstream/main.go | 3 +- src/jetstream/middleware.go | 58 ++++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/jetstream/main.go b/src/jetstream/main.go index 15d16803a0..d63573ff27 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -1119,7 +1119,7 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, needSetupMiddleware bool) { // API endpoints with Swagger documentation and accessible with an API key that require admin permissions stableAdminAPIGroup := stableAPIGroup if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled { - stableAdminAPIGroup.Use(p.endpointMiddleware) + stableAdminAPIGroup.Use(p.endpointAdminMiddleware) } else { stableAdminAPIGroup.Use(p.adminMiddleware) } @@ -1127,6 +1127,7 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, needSetupMiddleware bool) { // route endpoint creation requests to respecive plugins stableAdminAPIGroup.POST("/endpoints", p.pluginRegisterRouter) + stableAdminAPIGroup.Use(p.endpointUpdateDeleteMiddleware) // Apply edits for the given endpoint stableAdminAPIGroup.POST("/endpoints/:id", p.updateEndpoint) stableAdminAPIGroup.DELETE("/endpoints/:id", p.unregisterCluster) diff --git a/src/jetstream/middleware.go b/src/jetstream/middleware.go index 1912de0045..e40c91b66f 100644 --- a/src/jetstream/middleware.go +++ b/src/jetstream/middleware.go @@ -252,8 +252,8 @@ func (p *portalProxy) adminMiddleware(h echo.HandlerFunc) echo.HandlerFunc { } } -// endpointMiddleware - checks if user is admin or endpointadmin and has the necessary rights to edit the endpoint -func (p *portalProxy) endpointMiddleware(h echo.HandlerFunc) echo.HandlerFunc { +// endpointAdminMiddleware - checks if user is admin or endpointadmin +func (p *portalProxy) endpointAdminMiddleware(h echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { userID, err := p.GetSessionValue(c, "user_id") if err != nil { @@ -265,39 +265,45 @@ func (p *portalProxy) endpointMiddleware(h echo.HandlerFunc) echo.HandlerFunc { return c.NoContent(http.StatusUnauthorized) } - // TODO remove hard coded value endpointAdmin := strings.Contains(strings.Join(u.Scopes, ""), "stratos.endpointadmin") + if endpointAdmin == false && u.Admin == false { return handleSessionError(p.Config, c, errors.New("Unauthorized"), false, "You must be a Stratos admin or endpoint-admin to access this API") } - // if id exists, then it's not a CREATE request + return h(c) + } +} + +// endpointUpdateDeleteMiddleware - checks if user has necessary permissions to modify endpoint +func (p *portalProxy) endpointUpdateDeleteMiddleware(h echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + userID, err := p.GetSessionValue(c, "user_id") + if err != nil { + return c.NoContent(http.StatusUnauthorized) + } + + u, err := p.StratosAuthService.GetUser(userID.(string)) + if err != nil { + return c.NoContent(http.StatusUnauthorized) + } + endpointID := c.Param("id") - if len(endpointID) != 0 { - cnsiRecord, err := p.GetCNSIRecord(endpointID) - if err != nil { - return c.NoContent(http.StatusUnauthorized) - } - // if Creator is not set, then its a legacy admin-endpoint - creator := &interfaces.ConnectedUser{ - Admin: true, - } - if len(cnsiRecord.Creator) != 0 { - creatorRecord, err := p.StratosAuthService.GetUser(cnsiRecord.Creator) - if err != nil { - return c.NoContent(http.StatusUnauthorized) - } - creator = creatorRecord - } + cnsiRecord, err := p.GetCNSIRecord(endpointID) + if err != nil { + return c.NoContent(http.StatusUnauthorized) + } - if creator.Admin == true && u.Admin == false { - return handleSessionError(p.Config, c, errors.New("Unauthorized"), false, "You must be Stratos admin to modify this endpoint.") - } + // endpoint created by admin when no id is saved + adminEndpoint := len(cnsiRecord.Creator) == 0 - if creator.Admin == false && u.Admin == false && len(creator.GUID) != 0 && creator.GUID != userID.(string) { - return handleSessionError(p.Config, c, errors.New("Unauthorized"), false, "Endpoint-admins are not allowed to modify endpoints created by other endpoint-admins.") - } + if adminEndpoint && !u.Admin { + return handleSessionError(p.Config, c, errors.New("Unauthorized"), false, "You must be Stratos admin to modify this endpoint.") + } + + if !adminEndpoint && !u.Admin && cnsiRecord.Creator != userID.(string) { + return handleSessionError(p.Config, c, errors.New("Unauthorized"), false, "Endpoint-admins are not allowed to modify endpoints created by other endpoint-admins.") } return h(c) From 9357fc4869e99b77cd5723e4d414f25b52cdd97d Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 18 Feb 2021 10:41:47 +0100 Subject: [PATCH 033/101] Simplify buildCNSIList (#4753) Signed-off-by: Thomas Quandt --- src/jetstream/cnsi.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 9136ed38b5..dce648a2bd 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -285,10 +285,10 @@ func (p *portalProxy) buildCNSIList(c echo.Context) ([]*interfaces.CNSIRecord, e if u.Admin { return p.ListEndpoints() - } else { - if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.AdminOnly { - return p.ListAdminEndpoints(userID.(string)) - } + } + + if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.AdminOnly { + return p.ListAdminEndpoints(userID.(string)) } } return p.ListAdminEndpoints("") From 79400cddc672cd8c6d527bd2a73bcbf50d23bf21 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 18 Feb 2021 11:14:54 +0100 Subject: [PATCH 034/101] Simplify admin-check doing invites (#4753) Signed-off-by: Thomas Quandt --- src/jetstream/plugins/userinvite/auth.go | 26 ++++++------------------ 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/src/jetstream/plugins/userinvite/auth.go b/src/jetstream/plugins/userinvite/auth.go index d10740b1f1..343fb3877d 100644 --- a/src/jetstream/plugins/userinvite/auth.go +++ b/src/jetstream/plugins/userinvite/auth.go @@ -123,7 +123,6 @@ func (invite *UserInvite) refreshToken(clientID, clientSecret string, endpoint i func (invite *UserInvite) checkEndpointCreator(cfGUID string, c echo.Context) (interfaces.CNSIRecord, error) { endpoint, err := invite.portalProxy.GetCNSIRecord(cfGUID) if err != nil { - // Could find the endpoint return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( http.StatusBadRequest, "Can not find enpoint", @@ -131,25 +130,12 @@ func (invite *UserInvite) checkEndpointCreator(cfGUID string, c echo.Context) (i ) } - if len(endpoint.Creator) > 0 { - stratosAuthService := invite.portalProxy.GetStratosAuthService() - - creator, err := stratosAuthService.GetUser(endpoint.Creator) - if err != nil { - return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( - http.StatusBadRequest, - "Can not find creator account", - "Can not find creator account: %s", endpoint.Creator, - ) - } - - if !creator.Admin { - return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( - http.StatusBadRequest, - "Not an admin endpoint", - "Not an admin endpoint: %s", cfGUID, - ) - } + if len(endpoint.Creator) != 0 { + return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( + http.StatusBadRequest, + "Not an admin endpoint", + "Not an admin endpoint: %s", cfGUID, + ) } return endpoint, nil From 8f12474c30bdeb3c0b9f6ef099c8188d66b8761c Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 18 Feb 2021 12:10:26 +0100 Subject: [PATCH 035/101] Add creator to frontend mock endpoints (#4753) Signed-off-by: Thomas Quandt --- ...rent-user-permissions-and-cfchecker.service.spec.ts | 8 ++++++++ .../current-user-permissions.service.spec.ts | 8 ++++++++ .../packages/store/testing/src/store-test-helper.ts | 10 +++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/frontend/packages/cloud-foundry/src/shared/services/current-user-permissions-and-cfchecker.service.spec.ts b/src/frontend/packages/cloud-foundry/src/shared/services/current-user-permissions-and-cfchecker.service.spec.ts index 0852f7e8b2..e94acc141f 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/services/current-user-permissions-and-cfchecker.service.spec.ts +++ b/src/frontend/packages/cloud-foundry/src/shared/services/current-user-permissions-and-cfchecker.service.spec.ts @@ -64,6 +64,10 @@ describe('CurrentUserPermissionsService with CF checker', () => { CfScopeStrings.CF_READ_SCOPE, ] }, + creator: { + name: 'admin', + admin: true + }, metricsAvailable: false, connectionStatus: 'connected', system_shared_token: false, @@ -102,6 +106,10 @@ describe('CurrentUserPermissionsService with CF checker', () => { StratosScopeStrings.SCIM_READ ] }, + creator: { + name: 'admin', + admin: true + }, metricsAvailable: false, connectionStatus: 'connected', system_shared_token: false, diff --git a/src/frontend/packages/core/src/core/permissions/current-user-permissions.service.spec.ts b/src/frontend/packages/core/src/core/permissions/current-user-permissions.service.spec.ts index 9d7f3bb0af..71f2e804c9 100644 --- a/src/frontend/packages/core/src/core/permissions/current-user-permissions.service.spec.ts +++ b/src/frontend/packages/core/src/core/permissions/current-user-permissions.service.spec.ts @@ -50,6 +50,10 @@ describe('CurrentUserPermissionsService', () => { StratosScopeStrings.STRATOS_CHANGE_PASSWORD, ] }, + creator: { + name: 'admin', + admin: true + }, metricsAvailable: false, connectionStatus: 'connected', system_shared_token: false, @@ -83,6 +87,10 @@ describe('CurrentUserPermissionsService', () => { StratosScopeStrings.SCIM_READ ] }, + creator: { + name: 'admin', + admin: true + }, metricsAvailable: false, connectionStatus: 'connected', system_shared_token: false, diff --git a/src/frontend/packages/store/testing/src/store-test-helper.ts b/src/frontend/packages/store/testing/src/store-test-helper.ts index 92440c6f7a..919142e94c 100644 --- a/src/frontend/packages/store/testing/src/store-test-helper.ts +++ b/src/frontend/packages/store/testing/src/store-test-helper.ts @@ -46,7 +46,11 @@ export const testSCFEndpoint: EndpointModel = { cnsi_type: 'cf', system_shared_token: false, sso_allowed: false, - metricsAvailable: false + metricsAvailable: false, + creator: { + name: 'admin', + admin: true + } }; export const testSessionData: SessionData = { @@ -299,6 +303,10 @@ function getDefaultInitialTestStoreState(): AppState { name: 'admin', admin: true }, + creator: { + name: 'admin', + admin: true + }, connectionStatus: 'connected', system_shared_token: false, metricsAvailable: false From 5ed5d55289d35ecff6e66ac8a46cee3b15bf0ee3 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 18 Feb 2021 15:08:18 +0100 Subject: [PATCH 036/101] Fix unit tests and code (#4753) * Add missing providers to frontend unit tests * Fix two errors outside of tests Signed-off-by: Thomas Quandt --- .../packages/core/src/core/endpoints.service.spec.ts | 4 +++- .../entity-favorite-star.component.spec.ts | 4 +++- .../create-endpoint-cf-step-1.component.spec.ts | 7 +++++-- .../create-endpoint/create-endpoint.component.spec.ts | 3 ++- .../packages/core/src/shared/services/session.service.ts | 4 ++-- .../packages/core/src/shared/user-permission.directive.ts | 8 +++++--- 6 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/frontend/packages/core/src/core/endpoints.service.spec.ts b/src/frontend/packages/core/src/core/endpoints.service.spec.ts index 3ac576abcd..8788c3d682 100644 --- a/src/frontend/packages/core/src/core/endpoints.service.spec.ts +++ b/src/frontend/packages/core/src/core/endpoints.service.spec.ts @@ -3,6 +3,7 @@ import { createBasicStoreModule } from '@stratosui/store/testing'; import { PaginationMonitorFactory } from '../../../store/src/monitors/pagination-monitor.factory'; import { CoreTestingModule } from '../../test-framework/core-test.modules'; +import { SessionService } from '../shared/services/session.service'; import { CoreModule } from './core.module'; import { EndpointsService } from './endpoints.service'; import { UtilsService } from './utils.service'; @@ -13,7 +14,8 @@ describe('EndpointsService', () => { providers: [ EndpointsService, UtilsService, - PaginationMonitorFactory + PaginationMonitorFactory, + SessionService ], imports: [ CoreModule, diff --git a/src/frontend/packages/core/src/core/entity-favorite-star/entity-favorite-star.component.spec.ts b/src/frontend/packages/core/src/core/entity-favorite-star/entity-favorite-star.component.spec.ts index 5b0c078512..755a43f5e2 100644 --- a/src/frontend/packages/core/src/core/entity-favorite-star/entity-favorite-star.component.spec.ts +++ b/src/frontend/packages/core/src/core/entity-favorite-star/entity-favorite-star.component.spec.ts @@ -8,6 +8,7 @@ import { UserFavoriteManager } from '../../../../store/src/user-favorite-manager import { BaseTestModulesNoShared } from '../../../test-framework/core-test.helper'; import { ConfirmationDialogService } from '../../shared/components/confirmation-dialog.service'; import { DialogConfirmComponent } from '../../shared/components/dialog-confirm/dialog-confirm.component'; +import { SessionService } from '../../shared/services/session.service'; import { EntityFavoriteStarComponent } from './entity-favorite-star.component'; describe('EntityFavoriteStarComponent', () => { @@ -28,7 +29,8 @@ describe('EntityFavoriteStarComponent', () => { overlayContainerElement = document.createElement('div'); return { getContainerElement: () => overlayContainerElement }; } - } + }, + SessionService ], declarations: [ DialogConfirmComponent diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.spec.ts index ee8c879141..5573da963f 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.spec.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.spec.ts @@ -7,6 +7,7 @@ import { CoreTestingModule } from '../../../../../test-framework/core-test.modul import { CoreModule } from '../../../../core/core.module'; import { SharedModule } from '../../../../shared/shared.module'; import { CreateEndpointCfStep1Component } from './create-endpoint-cf-step-1.component'; +import { CurrentUserPermissionsService } from '../../../../core/permissions/current-user-permissions.service'; describe('CreateEndpointCfStep1Component', () => { let component: CreateEndpointCfStep1Component; @@ -29,8 +30,10 @@ describe('CreateEndpointCfStep1Component', () => { queryParams: {}, params: { type: 'metrics' } } - } - }] + }, + }, + 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 a074af1f7b..14a0724331 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 @@ -47,8 +47,9 @@ describe('CreateEndpointComponent', () => { subtype: null } } - } + }, }, + CurrentUserPermissionsService, TabNavService, SidePanelService, CurrentUserPermissionsService diff --git a/src/frontend/packages/core/src/shared/services/session.service.ts b/src/frontend/packages/core/src/shared/services/session.service.ts index 4341d5481a..3dcc0e043d 100644 --- a/src/frontend/packages/core/src/shared/services/session.service.ts +++ b/src/frontend/packages/core/src/shared/services/session.service.ts @@ -22,14 +22,14 @@ export class SessionService { userEndpointsEnabled(): Observable { return this.store.select(selectSessionData()).pipe( first(), - map(sessionData => sessionData.config.UserEndpointsEnabled === UserEndpointsEnabled.ENABLED ? true : false) + map(sessionData => sessionData && sessionData.config.UserEndpointsEnabled === UserEndpointsEnabled.ENABLED ? true : false) ); } userEndpointsNotDisabled(): Observable { return this.store.select(selectSessionData()).pipe( first(), - map(sessionData => sessionData.config.UserEndpointsEnabled !== UserEndpointsEnabled.DISABLED ? true : false) + map(sessionData => sessionData && sessionData.config.UserEndpointsEnabled !== UserEndpointsEnabled.DISABLED ? true : false) ); } } diff --git a/src/frontend/packages/core/src/shared/user-permission.directive.ts b/src/frontend/packages/core/src/shared/user-permission.directive.ts index 2cfa77f23a..1effeda211 100644 --- a/src/frontend/packages/core/src/shared/user-permission.directive.ts +++ b/src/frontend/packages/core/src/shared/user-permission.directive.ts @@ -26,9 +26,11 @@ export class UserPermissionDirective implements OnDestroy, OnInit { public ngOnInit() { // execute a permission check for every give permissiontype let $permissionChecks: Observable[]; - $permissionChecks = this.appUserPermission.map((permission: PermissionTypes) => { - return this.currentUserPermissionsService.can(permission, this.appUserPermissionEndpointGuid); - }); + if (this.appUserPermission) { + $permissionChecks = this.appUserPermission.map((permission: PermissionTypes) => { + return this.currentUserPermissionsService.can(permission, this.appUserPermissionEndpointGuid); + }); + } // permit user if one check results true this.canSub = combineLatest($permissionChecks).pipe( From 93bbedaedb2c94759a2c1fd999f8e190f00587f2 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 18 Feb 2021 15:17:48 +0100 Subject: [PATCH 037/101] Add UserEndpointsEnabled check for middleware (#4753) Signed-off-by: Thomas Quandt --- src/jetstream/main.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/jetstream/main.go b/src/jetstream/main.go index d63573ff27..9224b65984 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -1127,7 +1127,11 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, needSetupMiddleware bool) { // route endpoint creation requests to respecive plugins stableAdminAPIGroup.POST("/endpoints", p.pluginRegisterRouter) - stableAdminAPIGroup.Use(p.endpointUpdateDeleteMiddleware) + // do additional checks if user endpoints are enabled + if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled { + stableAdminAPIGroup.Use(p.endpointUpdateDeleteMiddleware) + } + // Apply edits for the given endpoint stableAdminAPIGroup.POST("/endpoints/:id", p.updateEndpoint) stableAdminAPIGroup.DELETE("/endpoints/:id", p.unregisterCluster) From e85cd6d7e95737ecc5d873bbd0c3cf920c364689 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Fri, 19 Feb 2021 09:53:40 +0100 Subject: [PATCH 038/101] Fix frontend unit tests (#4753) Signed-off-by: Thomas Quandt --- .../git-registration/git-registration.component.spec.ts | 2 ++ .../components/git-registration/git-registration.component.ts | 4 ++-- .../chart-details-usage/chart-details-usage.component.spec.ts | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.spec.ts b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.spec.ts index 55c747e3fc..a5b820ecce 100644 --- a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.spec.ts +++ b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.spec.ts @@ -3,6 +3,7 @@ import { ActivatedRoute } from '@angular/router'; import { SharedModule } from '@stratosui/core'; import { getGitHubAPIURL, gitEntityCatalog, GITHUB_API_URL, GitSCMService } from '@stratosui/git'; import { CATALOGUE_ENTITIES } from '@stratosui/store'; +import { createBasicStoreModule } from '@stratosui/store/testing'; import { BaseTestModulesNoShared } from '../../../../../core/test-framework/core-test.helper'; import { GitRegistrationComponent } from './git-registration.component'; @@ -16,6 +17,7 @@ describe('GitRegistrationComponent', () => { declarations: [ GitRegistrationComponent ], imports: [ ...BaseTestModulesNoShared, + createBasicStoreModule(), SharedModule, ], providers: [ diff --git a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts index 37c8b95f32..9bde31b181 100644 --- a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts +++ b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts @@ -1,11 +1,11 @@ import { Component, OnDestroy } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; -import { StratosCurrentUserPermissions } from 'frontend/packages/core/src/core/permissions/stratos-user-permissions.checker'; -import { SessionService } from 'frontend/packages/core/src/shared/services/session.service'; import { Observable, Subscription } from 'rxjs'; import { filter, first, map, pairwise } from 'rxjs/operators'; +import { StratosCurrentUserPermissions } from '../../../../../core/src/core/permissions/stratos-user-permissions.checker'; +import { SessionService } from '../../../../../core/src/shared/services/session.service'; import { EndpointsService } from '../../../../../core/src/core/endpoints.service'; import { getIdFromRoute } from '../../../../../core/src/core/utils.service'; import { ConnectEndpointConfig } from '../../../../../core/src/features/endpoints/connect.service'; diff --git a/src/frontend/packages/kubernetes/src/helm/monocular/chart-details/chart-details-usage/chart-details-usage.component.spec.ts b/src/frontend/packages/kubernetes/src/helm/monocular/chart-details/chart-details-usage/chart-details-usage.component.spec.ts index dcd88dc1ab..b6af842d7f 100644 --- a/src/frontend/packages/kubernetes/src/helm/monocular/chart-details/chart-details-usage/chart-details-usage.component.spec.ts +++ b/src/frontend/packages/kubernetes/src/helm/monocular/chart-details/chart-details-usage/chart-details-usage.component.spec.ts @@ -1,5 +1,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { TestBed } from '@angular/core/testing'; +import { SessionService } from '../../../../../../core/src/shared/services/session.service'; import { EndpointsService } from '../../../../../../core/src/core/endpoints.service'; import { UtilsService } from '../../../../../../core/src/core/utils.service'; @@ -15,7 +16,8 @@ describe('Component: ChartDetailsUsage', () => { providers: [ EndpointsService, UtilsService, - PaginationMonitorFactory + PaginationMonitorFactory, + SessionService ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); From 5c6c9058146eac23014a22a7aef2ae18a6db8d9e Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Fri, 19 Feb 2021 15:49:37 +0000 Subject: [PATCH 039/101] =?UTF-8?q?E2E=20test=20debug=20-=20This=20works?= =?UTF-8?q?=20fine=20locally=20```=20=20=20=20=20=20=20=20=2000:00:50=20-?= =?UTF-8?q?=20=E2=9C=97=20should=20reach=20the=20endpoints=20page=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20-=20Failed:=20Cannot=20read=20property?= =?UTF-8?q?=20'forEach'=20of=20null=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20at=20/home/travis/build/cloudfoundry/stratos/src/test-e2e?= =?UTF-8?q?/helpers/reset-helpers.ts:142:12=20```?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts | 2 +- src/test-e2e/helpers/reset-helpers.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts b/src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts index 97bd995955..84f1b6828c 100644 --- a/src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts +++ b/src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts @@ -12,7 +12,7 @@ import { SnackBarPo } from '../po/snackbar.po'; import { ConnectDialogComponent } from './connect-dialog.po'; import { EndpointMetadata, EndpointsPage } from './endpoints.po'; -describe('Endpoints', () => { +fdescribe('Endpoints', () => { const endpointsPage = new EndpointsPage(); const snackBar = new SnackBarPo(); diff --git a/src/test-e2e/helpers/reset-helpers.ts b/src/test-e2e/helpers/reset-helpers.ts index 4f3579fc53..6f4caba4e6 100644 --- a/src/test-e2e/helpers/reset-helpers.ts +++ b/src/test-e2e/helpers/reset-helpers.ts @@ -133,11 +133,15 @@ export class ResetsHelpers { */ removeAllEndpoints(req) { return reqHelpers.sendRequest(req, { method: 'GET', url: 'api/v1/endpoints' }).then((data) => { + console.warn('reset-helpers.ts: removeAllEndpoints: raw response', data); + if (!data || !data.length) { return; } data = data.trim(); + console.warn('reset-helpers.ts: removeAllEndpoints: trimmed response', data); data = JSON.parse(data); + console.warn('reset-helpers.ts: removeAllEndpoints: parsed response', data); const p = promise.fulfilled({}); data.forEach((c) => { p.then(() => reqHelpers.sendRequest(req, { method: 'DELETE', url: 'api/v1/endpoints/' + c.guid }, null, {})); From 1bf8682ca3c03407a353ac1c874caccf68d3ba83 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Fri, 19 Feb 2021 16:48:19 +0000 Subject: [PATCH 040/101] E2E test debug - round 2 --- src/jetstream/cnsi.go | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index dce648a2bd..7245c79161 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -271,27 +271,40 @@ func (p *portalProxy) doUnregisterCluster(cnsiGUID string) error { func (p *portalProxy) buildCNSIList(c echo.Context) ([]*interfaces.CNSIRecord, error) { log.Debug("buildCNSIList") - + log.Warnf("buildCNSIList: Started") if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled { userID, err := p.GetSessionValue(c, "user_id") if err != nil { + log.Warnf("buildCNSIList: 1: %+v", err) return nil, err } u, err := p.StratosAuthService.GetUser(userID.(string)) if err != nil { + log.Warnf("buildCNSIList: 2: %+v", err) return nil, err } if u.Admin { - return p.ListEndpoints() + log.Warnf("buildCNSIList: 3") + res, err := p.ListEndpoints() + log.Warnf("buildCNSIList: 3 res: %+v", res) + return res, err } + // TODO: RC Something doesn't feel right here.... if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.AdminOnly { - return p.ListAdminEndpoints(userID.(string)) + log.Warnf("buildCNSIList: 4") + res, err := p.ListAdminEndpoints(userID.(string)) + log.Warnf("buildCNSIList: 4: res: %+v", res) + + return res, err } } - return p.ListAdminEndpoints("") + log.Warnf("buildCNSIList: 5") + res, err := p.ListAdminEndpoints("") + log.Warnf("buildCNSIList: 5: %+v", res) + return res, err } func (p *portalProxy) ListEndpoints() ([]*interfaces.CNSIRecord, error) { @@ -315,7 +328,7 @@ func (p *portalProxy) ListEndpoints() ([]*interfaces.CNSIRecord, error) { //return a CNSI list with endpoints created by the current endpointadmin and all admins func (p *portalProxy) ListAdminEndpoints(userID string) ([]*interfaces.CNSIRecord, error) { log.Debug("ListAdminEndpoints") - + log.Warnf("ListAdminEndpoints: started") var cnsiList []*interfaces.CNSIRecord var userList []string var err error @@ -324,20 +337,27 @@ func (p *portalProxy) ListAdminEndpoints(userID string) ([]*interfaces.CNSIRecor if len(userID) != 0 { userList = append(userList, "") } + log.Warnf("ListAdminEndpoints: userList: %+v: ", userList) //get a cnsi list from every admin found and given userID cnsiRepo, err := p.GetStoreFactory().EndpointStore() if err != nil { - return cnsiList, fmt.Errorf("listRegisteredCNSIs: %s", err) + return nil, fmt.Errorf("listRegisteredCNSIs: %s", err) } for _, id := range userList { creatorList, err := cnsiRepo.ListByCreator(id, p.Config.EncryptionKeyInBytes) + log.Warnf("ListAdminEndpoints: creatorList: %+v: ", creatorList) + log.Warnf("ListAdminEndpoints: creatorList err: %+v: ", err) if err != nil { - return creatorList, err + return nil, err } + log.Warnf("ListAdminEndpoints: cnsiList: b %+v: ", cnsiList) cnsiList = append(cnsiList, creatorList...) + log.Warnf("ListAdminEndpoints: cnsiList: a %+v: ", cnsiList) } + log.Warnf("ListAdminEndpoints: Completed %+v: ", cnsiList) + return cnsiList, nil } @@ -379,6 +399,7 @@ func (p *portalProxy) listCNSIByAPIEndpoint(apiEndpoint string) ([]*interfaces.C // @Router /endpoints [get] func (p *portalProxy) listCNSIs(c echo.Context) error { log.Debug("listCNSIs") + log.Warn("listCNSIs: Started") cnsiList, err := p.buildCNSIList(c) if err != nil { return interfaces.NewHTTPShadowError( @@ -387,14 +408,16 @@ func (p *portalProxy) listCNSIs(c echo.Context) error { "Failed to retrieve list of CNSIs: %v", err, ) } - + log.Warnf("listCNSIs: Res Object %+v", cnsiList) jsonString, err := marshalCNSIlist(cnsiList) if err != nil { return err } + log.Warnf("listCNSIs: Res String %+s", jsonString) c.Response().Header().Set("Content-Type", "application/json") c.Response().Write(jsonString) + log.Warn("listCNSIs: Completed") return nil } From f636f74a0798323b1d6771d9e902e3ec21d6a225 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Fri, 19 Feb 2021 17:12:19 +0000 Subject: [PATCH 041/101] Fix core e2e failures --- src/jetstream/cnsi.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 7245c79161..a9ef290c53 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -329,7 +329,8 @@ func (p *portalProxy) ListEndpoints() ([]*interfaces.CNSIRecord, error) { func (p *portalProxy) ListAdminEndpoints(userID string) ([]*interfaces.CNSIRecord, error) { log.Debug("ListAdminEndpoints") log.Warnf("ListAdminEndpoints: started") - var cnsiList []*interfaces.CNSIRecord + // Initialise cnsiList to ensure empty struct (marshals to null) is not returned + cnsiList := []*interfaces.CNSIRecord{} var userList []string var err error From 74309c986ec1564c08a66eef878dca923a4f4c79 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Fri, 19 Feb 2021 17:35:27 +0000 Subject: [PATCH 042/101] Revert "E2E test debug - round 2" This reverts commit 7b17acf37147d8d2b989f4d8d476f3f1511607f9. --- src/jetstream/cnsi.go | 38 +++++++------------------------------- 1 file changed, 7 insertions(+), 31 deletions(-) diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index a9ef290c53..1250f58d3c 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -271,40 +271,27 @@ func (p *portalProxy) doUnregisterCluster(cnsiGUID string) error { func (p *portalProxy) buildCNSIList(c echo.Context) ([]*interfaces.CNSIRecord, error) { log.Debug("buildCNSIList") - log.Warnf("buildCNSIList: Started") + if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled { userID, err := p.GetSessionValue(c, "user_id") if err != nil { - log.Warnf("buildCNSIList: 1: %+v", err) return nil, err } u, err := p.StratosAuthService.GetUser(userID.(string)) if err != nil { - log.Warnf("buildCNSIList: 2: %+v", err) return nil, err } if u.Admin { - log.Warnf("buildCNSIList: 3") - res, err := p.ListEndpoints() - log.Warnf("buildCNSIList: 3 res: %+v", res) - return res, err + return p.ListEndpoints() } - // TODO: RC Something doesn't feel right here.... if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.AdminOnly { - log.Warnf("buildCNSIList: 4") - res, err := p.ListAdminEndpoints(userID.(string)) - log.Warnf("buildCNSIList: 4: res: %+v", res) - - return res, err + return p.ListAdminEndpoints(userID.(string)) } } - log.Warnf("buildCNSIList: 5") - res, err := p.ListAdminEndpoints("") - log.Warnf("buildCNSIList: 5: %+v", res) - return res, err + return p.ListAdminEndpoints("") } func (p *portalProxy) ListEndpoints() ([]*interfaces.CNSIRecord, error) { @@ -328,7 +315,6 @@ func (p *portalProxy) ListEndpoints() ([]*interfaces.CNSIRecord, error) { //return a CNSI list with endpoints created by the current endpointadmin and all admins func (p *portalProxy) ListAdminEndpoints(userID string) ([]*interfaces.CNSIRecord, error) { log.Debug("ListAdminEndpoints") - log.Warnf("ListAdminEndpoints: started") // Initialise cnsiList to ensure empty struct (marshals to null) is not returned cnsiList := []*interfaces.CNSIRecord{} var userList []string @@ -338,27 +324,20 @@ func (p *portalProxy) ListAdminEndpoints(userID string) ([]*interfaces.CNSIRecor if len(userID) != 0 { userList = append(userList, "") } - log.Warnf("ListAdminEndpoints: userList: %+v: ", userList) //get a cnsi list from every admin found and given userID cnsiRepo, err := p.GetStoreFactory().EndpointStore() if err != nil { - return nil, fmt.Errorf("listRegisteredCNSIs: %s", err) + return cnsiList, fmt.Errorf("listRegisteredCNSIs: %s", err) } for _, id := range userList { creatorList, err := cnsiRepo.ListByCreator(id, p.Config.EncryptionKeyInBytes) - log.Warnf("ListAdminEndpoints: creatorList: %+v: ", creatorList) - log.Warnf("ListAdminEndpoints: creatorList err: %+v: ", err) if err != nil { - return nil, err + return creatorList, err } - log.Warnf("ListAdminEndpoints: cnsiList: b %+v: ", cnsiList) cnsiList = append(cnsiList, creatorList...) - log.Warnf("ListAdminEndpoints: cnsiList: a %+v: ", cnsiList) } - log.Warnf("ListAdminEndpoints: Completed %+v: ", cnsiList) - return cnsiList, nil } @@ -400,7 +379,6 @@ func (p *portalProxy) listCNSIByAPIEndpoint(apiEndpoint string) ([]*interfaces.C // @Router /endpoints [get] func (p *portalProxy) listCNSIs(c echo.Context) error { log.Debug("listCNSIs") - log.Warn("listCNSIs: Started") cnsiList, err := p.buildCNSIList(c) if err != nil { return interfaces.NewHTTPShadowError( @@ -409,16 +387,14 @@ func (p *portalProxy) listCNSIs(c echo.Context) error { "Failed to retrieve list of CNSIs: %v", err, ) } - log.Warnf("listCNSIs: Res Object %+v", cnsiList) + jsonString, err := marshalCNSIlist(cnsiList) if err != nil { return err } - log.Warnf("listCNSIs: Res String %+s", jsonString) c.Response().Header().Set("Content-Type", "application/json") c.Response().Write(jsonString) - log.Warn("listCNSIs: Completed") return nil } From 8e2f495d83550138a4524636f34d1681918f305e Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Fri, 19 Feb 2021 17:35:49 +0000 Subject: [PATCH 043/101] Revert "E2E test debug" This reverts commit 8779158297e18770c54631f6230203c9ec26ecb9. --- src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts | 2 +- src/test-e2e/helpers/reset-helpers.ts | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts b/src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts index 84f1b6828c..97bd995955 100644 --- a/src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts +++ b/src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts @@ -12,7 +12,7 @@ import { SnackBarPo } from '../po/snackbar.po'; import { ConnectDialogComponent } from './connect-dialog.po'; import { EndpointMetadata, EndpointsPage } from './endpoints.po'; -fdescribe('Endpoints', () => { +describe('Endpoints', () => { const endpointsPage = new EndpointsPage(); const snackBar = new SnackBarPo(); diff --git a/src/test-e2e/helpers/reset-helpers.ts b/src/test-e2e/helpers/reset-helpers.ts index 6f4caba4e6..4f3579fc53 100644 --- a/src/test-e2e/helpers/reset-helpers.ts +++ b/src/test-e2e/helpers/reset-helpers.ts @@ -133,15 +133,11 @@ export class ResetsHelpers { */ removeAllEndpoints(req) { return reqHelpers.sendRequest(req, { method: 'GET', url: 'api/v1/endpoints' }).then((data) => { - console.warn('reset-helpers.ts: removeAllEndpoints: raw response', data); - if (!data || !data.length) { return; } data = data.trim(); - console.warn('reset-helpers.ts: removeAllEndpoints: trimmed response', data); data = JSON.parse(data); - console.warn('reset-helpers.ts: removeAllEndpoints: parsed response', data); const p = promise.fulfilled({}); data.forEach((c) => { p.then(() => reqHelpers.sendRequest(req, { method: 'DELETE', url: 'api/v1/endpoints/' + c.guid }, null, {})); From b968bd8d48d4125c1b30faedfeffaa0581a8eb3f Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Mon, 22 Feb 2021 12:24:13 +0100 Subject: [PATCH 044/101] Fix frontend unit test (#4753) Signed-off-by: Thomas Quandt --- .../git-registration.component.spec.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.spec.ts b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.spec.ts index a5b820ecce..7b5e5df4ab 100644 --- a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.spec.ts +++ b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.spec.ts @@ -2,11 +2,11 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; import { SharedModule } from '@stratosui/core'; import { getGitHubAPIURL, gitEntityCatalog, GITHUB_API_URL, GitSCMService } from '@stratosui/git'; -import { CATALOGUE_ENTITIES } from '@stratosui/store'; -import { createBasicStoreModule } from '@stratosui/store/testing'; +import { CATALOGUE_ENTITIES, entityCatalog, TestEntityCatalog } from '@stratosui/store'; import { BaseTestModulesNoShared } from '../../../../../core/test-framework/core-test.helper'; import { GitRegistrationComponent } from './git-registration.component'; +import { generateStratosEntities } from '../../../../../store/src/stratos-entity-generator'; describe('GitRegistrationComponent', () => { let component: GitRegistrationComponent; @@ -17,13 +17,21 @@ describe('GitRegistrationComponent', () => { declarations: [ GitRegistrationComponent ], imports: [ ...BaseTestModulesNoShared, - createBasicStoreModule(), SharedModule, ], providers: [ { provide: GITHUB_API_URL, useFactory: getGitHubAPIURL }, GitSCMService, - { provide: CATALOGUE_ENTITIES, useFactory: () => gitEntityCatalog.allGitEntities(), multi: false }, + { + provide: CATALOGUE_ENTITIES, useFactory: () => { + const testEntityCatalog = entityCatalog as TestEntityCatalog; + testEntityCatalog.clear(); + return [ + ...generateStratosEntities(), + ...gitEntityCatalog.allGitEntities() + ]; + }, multi: false + }, { provide: ActivatedRoute, useValue: { From 8cd8d617847e1850e20283b96bcb45ff980215f8 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Mon, 22 Feb 2021 12:44:04 +0100 Subject: [PATCH 045/101] Fix code formatting (#4753) Signed-off-by: Thomas Quandt --- .../packages/core/src/shared/services/session.service.ts | 4 ++-- .../git-registration/git-registration.component.spec.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/frontend/packages/core/src/shared/services/session.service.ts b/src/frontend/packages/core/src/shared/services/session.service.ts index 3dcc0e043d..e3cf86c622 100644 --- a/src/frontend/packages/core/src/shared/services/session.service.ts +++ b/src/frontend/packages/core/src/shared/services/session.service.ts @@ -22,14 +22,14 @@ export class SessionService { userEndpointsEnabled(): Observable { return this.store.select(selectSessionData()).pipe( first(), - map(sessionData => sessionData && sessionData.config.UserEndpointsEnabled === UserEndpointsEnabled.ENABLED ? true : false) + map(sessionData => sessionData && sessionData.config.UserEndpointsEnabled === UserEndpointsEnabled.ENABLED) ); } userEndpointsNotDisabled(): Observable { return this.store.select(selectSessionData()).pipe( first(), - map(sessionData => sessionData && sessionData.config.UserEndpointsEnabled !== UserEndpointsEnabled.DISABLED ? true : false) + map(sessionData => sessionData && sessionData.config.UserEndpointsEnabled !== UserEndpointsEnabled.DISABLED) ); } } diff --git a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.spec.ts b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.spec.ts index 7b5e5df4ab..d5ab6a319d 100644 --- a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.spec.ts +++ b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.spec.ts @@ -22,7 +22,7 @@ describe('GitRegistrationComponent', () => { providers: [ { provide: GITHUB_API_URL, useFactory: getGitHubAPIURL }, GitSCMService, - { + { provide: CATALOGUE_ENTITIES, useFactory: () => { const testEntityCatalog = entityCatalog as TestEntityCatalog; testEntityCatalog.clear(); @@ -30,7 +30,7 @@ describe('GitRegistrationComponent', () => { ...generateStratosEntities(), ...gitEntityCatalog.allGitEntities() ]; - }, multi: false + }, multi: false }, { provide: ActivatedRoute, From 5a910e5ec0a8a8d8bfbca31262c4b07807827e9b Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Mon, 22 Feb 2021 12:46:10 +0100 Subject: [PATCH 046/101] Change disconnect permission to admin (#4876) Signed-off-by: Thomas Quandt --- .../list/list-types/endpoint/endpoint-list.helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts index 128667e9c1..5a8fa03b98 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts @@ -111,7 +111,7 @@ export class EndpointListHelper { label: 'Disconnect', description: ``, // Description depends on console user permission createVisible: (row$: Observable) => combineLatest([ - this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT), + this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT), row$ ]).pipe( map(([isAdmin, row]) => { From 7aaa68ced706cb508ac875eb8a7d4f7acd25ae6f Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Mon, 22 Feb 2021 12:50:12 +0100 Subject: [PATCH 047/101] Remove unused function listAuthToken (#4876) Signed-off-by: Thomas Quandt --- .../plugins/desktop/kubernetes/tokens.go | 4 - src/jetstream/plugins/desktop/tokens.go | 4 - src/jetstream/repository/interfaces/tokens.go | 1 - .../repository/tokens/pgsql_tokens.go | 75 ------------------- 4 files changed, 84 deletions(-) diff --git a/src/jetstream/plugins/desktop/kubernetes/tokens.go b/src/jetstream/plugins/desktop/kubernetes/tokens.go index eef50d3cfe..c2df81029a 100644 --- a/src/jetstream/plugins/desktop/kubernetes/tokens.go +++ b/src/jetstream/plugins/desktop/kubernetes/tokens.go @@ -14,10 +14,6 @@ func (d *TokenStore) FindAuthToken(userGUID string, encryptionKey []byte) (inter return d.store.FindAuthToken(userGUID, encryptionKey) } -func (d *TokenStore) ListAuthToken(encryptionKey []byte) ([]interfaces.TokenRecord, error) { - return d.store.ListAuthToken(encryptionKey) -} - func (d *TokenStore) SaveAuthToken(userGUID string, tokenRecord interfaces.TokenRecord, encryptionKey []byte) error { return d.store.SaveAuthToken(userGUID, tokenRecord, encryptionKey) } diff --git a/src/jetstream/plugins/desktop/tokens.go b/src/jetstream/plugins/desktop/tokens.go index 6c528e6af9..5063b912d1 100644 --- a/src/jetstream/plugins/desktop/tokens.go +++ b/src/jetstream/plugins/desktop/tokens.go @@ -17,10 +17,6 @@ func (d *TokenStore) FindAuthToken(userGUID string, encryptionKey []byte) (inter return d.store.FindAuthToken(userGUID, encryptionKey) } -func (d *TokenStore) ListAuthToken(encryptionKey []byte) ([]interfaces.TokenRecord, error) { - return d.store.ListAuthToken(encryptionKey) -} - func (d *TokenStore) SaveAuthToken(userGUID string, tokenRecord interfaces.TokenRecord, encryptionKey []byte) error { return d.store.SaveAuthToken(userGUID, tokenRecord, encryptionKey) } diff --git a/src/jetstream/repository/interfaces/tokens.go b/src/jetstream/repository/interfaces/tokens.go index b8b9f40a37..0467c8220b 100644 --- a/src/jetstream/repository/interfaces/tokens.go +++ b/src/jetstream/repository/interfaces/tokens.go @@ -10,7 +10,6 @@ type Token struct { // TokenRepository is an application of the repository pattern for storing tokens type TokenRepository interface { FindAuthToken(userGUID string, encryptionKey []byte) (TokenRecord, error) - ListAuthToken(encryptionKey []byte) ([]TokenRecord, error) SaveAuthToken(userGUID string, tokenRecord TokenRecord, encryptionKey []byte) error FindCNSIToken(cnsiGUID string, userGUID string, encryptionKey []byte) (TokenRecord, error) diff --git a/src/jetstream/repository/tokens/pgsql_tokens.go b/src/jetstream/repository/tokens/pgsql_tokens.go index f786e0e6df..89128b7839 100644 --- a/src/jetstream/repository/tokens/pgsql_tokens.go +++ b/src/jetstream/repository/tokens/pgsql_tokens.go @@ -16,10 +16,6 @@ var findAuthToken = `SELECT token_guid, auth_token, refresh_token, token_expiry, FROM tokens WHERE token_type = 'uaa' AND cnsi_guid = 'STRATOS' AND user_guid = $1` -var listAuthTokens = `SELECT token_guid, auth_token, refresh_token, token_expiry, auth_type, meta_data - FROM tokens - WHERE token_type = 'uaa' AND cnsi_guid = 'STRATOS'` - var countAuthTokens = `SELECT COUNT(*) FROM tokens WHERE token_type = 'uaa' AND cnsi_guid = 'STRATOS' AND user_guid = $1 ` @@ -88,7 +84,6 @@ func NewPgsqlTokenRepository(dcp *sql.DB) (interfaces.TokenRepository, error) { func InitRepositoryProvider(databaseProvider string) { // Modify the database statements if needed, for the given database type findAuthToken = datastore.ModifySQLStatement(findAuthToken, databaseProvider) - listAuthTokens = datastore.ModifySQLStatement(listAuthTokens, databaseProvider) countAuthTokens = datastore.ModifySQLStatement(countAuthTokens, databaseProvider) insertAuthToken = datastore.ModifySQLStatement(insertAuthToken, databaseProvider) updateAuthToken = datastore.ModifySQLStatement(updateAuthToken, databaseProvider) @@ -225,76 +220,6 @@ func (p *PgsqlTokenRepository) FindAuthToken(userGUID string, encryptionKey []by return *tr, nil } -// ListAuthToken - Return a list of Auth Tokens -func (p *PgsqlTokenRepository) ListAuthToken(encryptionKey []byte) ([]interfaces.TokenRecord, error) { - log.Debug("ListAuthTokens") - - var rows *sql.Rows - var err error - rows, err = p.db.Query(listAuthTokens) - if err != nil { - msg := "Unable to Find All CNSI tokens: %v" - if err == sql.ErrNoRows { - log.Debugf(msg, err) - } else { - log.Errorf(msg, err) - } - return make([]interfaces.TokenRecord, 0), fmt.Errorf(msg, err) - } - defer rows.Close() - - tokens := make([]interfaces.TokenRecord, 0) - for rows.Next() { - // temp vars to retrieve db data - var ( - tokenGUID sql.NullString - ciphertextAuthToken []byte - ciphertextRefreshToken []byte - tokenExpiry sql.NullInt64 - authType string - metadata sql.NullString - ) - - err := rows.Scan(&tokenGUID, &ciphertextAuthToken, &ciphertextRefreshToken, &tokenExpiry, &authType, &metadata) - if err != nil { - msg := "Unable to Find UAA token: %v" - log.Debugf(msg, err) - return make([]interfaces.TokenRecord, 0), fmt.Errorf(msg, err) - } - - log.Debug("Decrypting Auth Token") - plaintextAuthToken, err := crypto.DecryptToken(encryptionKey, ciphertextAuthToken) - if err != nil { - return make([]interfaces.TokenRecord, 0), err - } - - log.Debug("Decrypting Refresh Token") - plaintextRefreshToken, err := crypto.DecryptToken(encryptionKey, ciphertextRefreshToken) - if err != nil { - return make([]interfaces.TokenRecord, 0), err - } - - // Build a new TokenRecord based on the decrypted tokens - tr := new(interfaces.TokenRecord) - if tokenGUID.Valid { - tr.TokenGUID = tokenGUID.String - } - tr.AuthToken = plaintextAuthToken - tr.RefreshToken = plaintextRefreshToken - if tokenExpiry.Valid { - tr.TokenExpiry = tokenExpiry.Int64 - } - tr.AuthType = authType - if metadata.Valid { - tr.Metadata = metadata.String - } - - tokens = append(tokens, *tr) - } - - return tokens, nil -} - // SaveCNSIToken - Save the CNSI (UAA) token to the datastore func (p *PgsqlTokenRepository) SaveCNSIToken(cnsiGUID string, userGUID string, tr interfaces.TokenRecord, encryptionKey []byte) error { log.Debug("SaveCNSIToken") From 59a1daa9707e3999196b8066e2695d3ac79365d7 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Mon, 22 Feb 2021 13:51:24 +0100 Subject: [PATCH 048/101] Revert sys shared endpoint error msg (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/authcnsi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jetstream/authcnsi.go b/src/jetstream/authcnsi.go index 8c2ae209df..eb6ce4c2a5 100644 --- a/src/jetstream/authcnsi.go +++ b/src/jetstream/authcnsi.go @@ -166,7 +166,7 @@ func (p *portalProxy) DoLoginToCNSI(c echo.Context, cnsiGUID string, systemShare if systemSharedToken { user, err := p.StratosAuthService.GetUser(userID) if err != nil { - return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - could not check user") + return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect System Shared endpoint - could not check user") } // User needs to be an admin From f3d0e39fa277557704c619ffe7dab73f6f3f073f Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Mon, 22 Feb 2021 13:58:07 +0100 Subject: [PATCH 049/101] Allow invites with user-endpoints (#4876) Signed-off-by: Thomas Quandt --- .../cf/user-invites/user-invite.service.ts | 3 +-- src/jetstream/plugins/userinvite/admin.go | 5 ----- src/jetstream/plugins/userinvite/auth.go | 22 ------------------- 3 files changed, 1 insertion(+), 29 deletions(-) diff --git a/src/frontend/packages/cloud-foundry/src/features/cf/user-invites/user-invite.service.ts b/src/frontend/packages/cloud-foundry/src/features/cf/user-invites/user-invite.service.ts index ac8a10bd64..637a1cbd09 100644 --- a/src/frontend/packages/cloud-foundry/src/features/cf/user-invites/user-invite.service.ts +++ b/src/frontend/packages/cloud-foundry/src/features/cf/user-invites/user-invite.service.ts @@ -150,8 +150,7 @@ export class UserInviteService { ]).pipe( map(([cf, auth, endpoint]) => cf.global.isAdmin && - auth.sessionData['plugin-config'] && auth.sessionData['plugin-config'].userInvitationsEnabled === 'true' && - endpoint.entity.creator.admin) + auth.sessionData['plugin-config'] && auth.sessionData['plugin-config'].userInvitationsEnabled === 'true') ); } diff --git a/src/jetstream/plugins/userinvite/admin.go b/src/jetstream/plugins/userinvite/admin.go index b8cde28a39..72363560dc 100644 --- a/src/jetstream/plugins/userinvite/admin.go +++ b/src/jetstream/plugins/userinvite/admin.go @@ -79,11 +79,6 @@ func (invite *UserInvite) configure(c echo.Context) error { ) } - _, err := invite.checkEndpointCreator(cfGUID, c) - if err != nil { - return err - } - uaaRecord, _, err := invite.RefreshToken(cfGUID, clientID, clientSecret) if err != nil { return err diff --git a/src/jetstream/plugins/userinvite/auth.go b/src/jetstream/plugins/userinvite/auth.go index 343fb3877d..592b0f8561 100644 --- a/src/jetstream/plugins/userinvite/auth.go +++ b/src/jetstream/plugins/userinvite/auth.go @@ -119,28 +119,6 @@ func (invite *UserInvite) refreshToken(clientID, clientSecret string, endpoint i return &uaaResponse, tokenRecord, nil } -// Check that there is an endpoint with the specified ID and if the creator is an admin -func (invite *UserInvite) checkEndpointCreator(cfGUID string, c echo.Context) (interfaces.CNSIRecord, error) { - endpoint, err := invite.portalProxy.GetCNSIRecord(cfGUID) - if err != nil { - return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( - http.StatusBadRequest, - "Can not find enpoint", - "Can not find enpoint: %s", cfGUID, - ) - } - - if len(endpoint.Creator) != 0 { - return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( - http.StatusBadRequest, - "Not an admin endpoint", - "Not an admin endpoint: %s", cfGUID, - ) - } - - return endpoint, nil -} - func (invite *UserInvite) checkEndpoint(cfGUID string) (interfaces.CNSIRecord, error) { // Check that there is an endpoint with the specified ID and that it is a Cloud Foundry endpoint endpoint, err := invite.portalProxy.GetCNSIRecord(cfGUID) From 1b804a8165350bd2cf5dd0057c50598b5108bffc Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Mon, 22 Feb 2021 14:14:14 +0100 Subject: [PATCH 050/101] Rename json UserEndpointsEnabled (#4876) Signed-off-by: Thomas Quandt --- .../packages/core/src/shared/services/session.service.ts | 4 ++-- src/frontend/packages/store/src/types/auth.types.ts | 2 +- src/jetstream/auth_test.go | 2 +- src/jetstream/repository/interfaces/structs.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/frontend/packages/core/src/shared/services/session.service.ts b/src/frontend/packages/core/src/shared/services/session.service.ts index e3cf86c622..d218b96bd4 100644 --- a/src/frontend/packages/core/src/shared/services/session.service.ts +++ b/src/frontend/packages/core/src/shared/services/session.service.ts @@ -22,14 +22,14 @@ export class SessionService { userEndpointsEnabled(): Observable { return this.store.select(selectSessionData()).pipe( first(), - map(sessionData => sessionData && sessionData.config.UserEndpointsEnabled === UserEndpointsEnabled.ENABLED) + map(sessionData => sessionData && sessionData.config.userEndpointsEnabled === UserEndpointsEnabled.ENABLED) ); } userEndpointsNotDisabled(): Observable { return this.store.select(selectSessionData()).pipe( first(), - map(sessionData => sessionData && sessionData.config.UserEndpointsEnabled !== UserEndpointsEnabled.DISABLED) + map(sessionData => sessionData && sessionData.config.userEndpointsEnabled !== UserEndpointsEnabled.DISABLED) ); } } diff --git a/src/frontend/packages/store/src/types/auth.types.ts b/src/frontend/packages/store/src/types/auth.types.ts index 7917ae0065..619e5665cf 100644 --- a/src/frontend/packages/store/src/types/auth.types.ts +++ b/src/frontend/packages/store/src/types/auth.types.ts @@ -39,7 +39,7 @@ export interface SessionDataConfig { APIKeysEnabled?: APIKeysEnabled; // Default value for Home View - show only favorited endpoints? homeViewShowFavoritesOnly?: boolean; - UserEndpointsEnabled?: UserEndpointsEnabled; + userEndpointsEnabled?: UserEndpointsEnabled; } export interface SessionData { endpoints?: SessionEndpoints; diff --git a/src/jetstream/auth_test.go b/src/jetstream/auth_test.go index 3cb873a347..5141eb0b51 100644 --- a/src/jetstream/auth_test.go +++ b/src/jetstream/auth_test.go @@ -795,7 +795,7 @@ func TestVerifySession(t *testing.T) { 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 = `{"status":"ok","error":"","data":{"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","homeViewShowFavoritesOnly":false,"UserEndpointsEnabled":"disabled"}}}` + var expectedBody = `{"status":"ok","error":"","data":{"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","homeViewShowFavoritesOnly":false,"userEndpointsEnabled":"disabled"}}}` Convey("Should contain expected body", func() { So(res, ShouldNotBeNil) diff --git a/src/jetstream/repository/interfaces/structs.go b/src/jetstream/repository/interfaces/structs.go index b3dc6df82c..8ecf6fafd2 100644 --- a/src/jetstream/repository/interfaces/structs.go +++ b/src/jetstream/repository/interfaces/structs.go @@ -241,7 +241,7 @@ type Info struct { ListAllowLoadMaxed bool `json:"listAllowLoadMaxed,omitempty"` APIKeysEnabled string `json:"APIKeysEnabled"` HomeViewShowFavoritesOnly bool `json:"homeViewShowFavoritesOnly"` - UserEndpointsEnabled string `json:"UserEndpointsEnabled"` + UserEndpointsEnabled string `json:"userEndpointsEnabled"` } `json:"config"` } From ba2360daabe9ea6216e43ec787edc4494bcc6d2f Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Mon, 22 Feb 2021 14:16:54 +0100 Subject: [PATCH 051/101] Edit config files (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/config.dev | 4 ++-- src/jetstream/config.example | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/jetstream/config.dev b/src/jetstream/config.dev index bfbd708bd9..522ec73ce9 100644 --- a/src/jetstream/config.dev +++ b/src/jetstream/config.dev @@ -12,7 +12,7 @@ SKIP_SSL_VALIDATION=true CONSOLE_PROXY_TLS_ADDRESS=:5443 CONSOLE_CLIENT=console CF_CLIENT=cf -UAA_ENDPOINT=http://localhost:8080 +UAA_ENDPOINT= CONSOLE_ADMIN_SCOPE=stratos.admin CF_ADMIN_ROLE=cloud_controller.admin ALLOWED_ORIGINS=http://nginx @@ -52,7 +52,7 @@ INVITE_USER_CLIENT_ID= INVITE_USER_CLIENT_SECRET= # Use local admin user rather than UAA users -AUTH_ENDPOINT_TYPE=remote +AUTH_ENDPOINT_TYPE=local LOCAL_USER=admin LOCAL_USER_PASSWORD=admin LOCAL_USER_SCOPE=stratos.admin diff --git a/src/jetstream/config.example b/src/jetstream/config.example index 1bffed8079..2c31f6c33e 100644 --- a/src/jetstream/config.example +++ b/src/jetstream/config.example @@ -39,6 +39,10 @@ ENABLE_TECH_PREVIEW=false # By default, only show favorites endpoints on the home view # HOME_VIEW_SHOW_FAVORITES_ONLY=false +# Enable users create user endpoints (disabled, admin_only, enabled). Default is disabled +# admin_only will enable admins to see created user endpoints, but users won't be able to create user endpoints or see them +USER_ENDPOINTS_ENABLED=disabled + # User Invites SMTP_FROM_ADDRESS=Stratos SMTP_HOST=127.0.0.1 From e1ff34a8df46b43019715676082af13c402452fd Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Mon, 22 Feb 2021 14:33:35 +0100 Subject: [PATCH 052/101] Check for matching creator & user during connect (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/authcnsi.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/jetstream/authcnsi.go b/src/jetstream/authcnsi.go index eb6ce4c2a5..2b9db5de5f 100644 --- a/src/jetstream/authcnsi.go +++ b/src/jetstream/authcnsi.go @@ -160,6 +160,10 @@ func (p *portalProxy) DoLoginToCNSI(c echo.Context, cnsiGUID string, systemShare if user.Admin { return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - admins are not allowed to connect to user created endpoints") } + + if cnsiRecord.Creator != userID { + return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - non-admins are not allowed to connect to endpoints created by other non-admins") + } } // Register as a system endpoint? From 2ad6bb902e7f083fe728af3595939ce661b59f2a Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Mon, 22 Feb 2021 14:38:19 +0100 Subject: [PATCH 053/101] Simplify code in cnsi.go (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/cnsi.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 1250f58d3c..658c3dca94 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -74,16 +74,15 @@ func (p *portalProxy) RegisterEndpoint(c echo.Context, fetchInfo interfaces.Info cnsiClientSecret = p.GetConfig().CFClientSecret } - sessionValue, err := p.GetSessionValue(c, "user_id") + uaaUserID, err := p.GetSessionStringValue(c, "user_id") if err != nil { return interfaces.NewHTTPShadowError( http.StatusInternalServerError, "Failed to get session user", "Failed to get session user: %v", err) } - uaaUserId := sessionValue.(string) - newCNSI, err := p.DoRegisterEndpoint(params.CNSIName, params.APIEndpoint, skipSSLValidation, cnsiClientId, cnsiClientSecret, uaaUserId, ssoAllowed, subType, overwriteEndpoints, fetchInfo) + newCNSI, err := p.DoRegisterEndpoint(params.CNSIName, params.APIEndpoint, skipSSLValidation, cnsiClientId, cnsiClientSecret, uaaUserID, ssoAllowed, subType, overwriteEndpoints, fetchInfo) if err != nil { return err } From 08f0448d5591d14ca6338049a3ecde62470bf328 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Mon, 22 Feb 2021 16:22:42 +0100 Subject: [PATCH 054/101] Create specific endpoints group for routes (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/main.go | 25 +++++++++++++++++-------- src/jetstream/middleware.go | 2 ++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/jetstream/main.go b/src/jetstream/main.go index 9224b65984..3e77ae7061 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -1118,23 +1118,32 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, needSetupMiddleware bool) { // API endpoints with Swagger documentation and accessible with an API key that require admin permissions stableAdminAPIGroup := stableAPIGroup + + stableEndpointAdminAPIGroup := stableAdminAPIGroup.Group("/endpoints") + if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled { - stableAdminAPIGroup.Use(p.endpointAdminMiddleware) + stableEndpointAdminAPIGroup.Use(p.endpointAdminMiddleware) } else { - stableAdminAPIGroup.Use(p.adminMiddleware) + stableEndpointAdminAPIGroup.Use(p.adminMiddleware) } // route endpoint creation requests to respecive plugins - stableAdminAPIGroup.POST("/endpoints", p.pluginRegisterRouter) + stableEndpointAdminAPIGroup.POST("", p.pluginRegisterRouter) + + // Use middleware in route directly, because documentation is faulty + // Apply middleware to group with .Use() when this issue is resolved: + // https://github.com/labstack/echo/issues/1519 // do additional checks if user endpoints are enabled if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled { - stableAdminAPIGroup.Use(p.endpointUpdateDeleteMiddleware) + // Apply edits for the given endpoint + stableEndpointAdminAPIGroup.POST("/:id", p.updateEndpoint, p.endpointUpdateDeleteMiddleware) + stableEndpointAdminAPIGroup.DELETE("/:id", p.unregisterCluster, p.endpointUpdateDeleteMiddleware) + } else { + // Apply edits for the given endpoint + stableEndpointAdminAPIGroup.POST("/:id", p.updateEndpoint) + stableEndpointAdminAPIGroup.DELETE("/:id", p.unregisterCluster) } - - // Apply edits for the given endpoint - stableAdminAPIGroup.POST("/endpoints/:id", p.updateEndpoint) - stableAdminAPIGroup.DELETE("/endpoints/:id", p.unregisterCluster) // sessionGroup.DELETE("/cnsis", p.removeCluster) // Serve up static resources diff --git a/src/jetstream/middleware.go b/src/jetstream/middleware.go index e40c91b66f..5fd3303c56 100644 --- a/src/jetstream/middleware.go +++ b/src/jetstream/middleware.go @@ -255,6 +255,7 @@ func (p *portalProxy) adminMiddleware(h echo.HandlerFunc) echo.HandlerFunc { // endpointAdminMiddleware - checks if user is admin or endpointadmin func (p *portalProxy) endpointAdminMiddleware(h echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { + log.Debug("endpointAdminMiddleware") userID, err := p.GetSessionValue(c, "user_id") if err != nil { return c.NoContent(http.StatusUnauthorized) @@ -278,6 +279,7 @@ func (p *portalProxy) endpointAdminMiddleware(h echo.HandlerFunc) echo.HandlerFu // endpointUpdateDeleteMiddleware - checks if user has necessary permissions to modify endpoint func (p *portalProxy) endpointUpdateDeleteMiddleware(h echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { + log.Debug("endpointUpdateDeleteMiddleware") userID, err := p.GetSessionValue(c, "user_id") if err != nil { return c.NoContent(http.StatusUnauthorized) From a587f797da37a4c741c3cc3a0c449f395a2dbec3 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Tue, 23 Feb 2021 18:02:39 +0100 Subject: [PATCH 055/101] Start adding additional backend-tests (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/cnsi_test.go | 79 +++++++++++++++++++++++++++++++ src/jetstream/mock_server_test.go | 12 +++++ 2 files changed, 91 insertions(+) diff --git a/src/jetstream/cnsi_test.go b/src/jetstream/cnsi_test.go index 4db23cad49..b2d68cfd45 100644 --- a/src/jetstream/cnsi_test.go +++ b/src/jetstream/cnsi_test.go @@ -3,11 +3,16 @@ package main import ( "errors" "net/http" + "net/http/httptest" "testing" "time" "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/satori/go.uuid" + . "github.com/smartystreets/goconvey/convey" "gopkg.in/DATA-DOG/go-sqlmock.v1" ) @@ -264,3 +269,77 @@ func TestGetCFv2InfoWithInvalidEndpoint(t *testing.T) { t.Error("getCFv2Info should not return a valid response when the endpoint is invalid.") } } + +func TestRegisterCFClusterWithUserEndpointsEnabled(t *testing.T) { + // execute this in parallel + t.Parallel() + + Convey("Test a convey", t, func() { + // mock StratosAuthService + ctrl := gomock.NewController(t) + mockStratosAuth := mock_interfaces.NewMockStratosAuth(ctrl) + defer ctrl.Finish() + + // setup mock DB, PortalProxy and mock StratosAuthService + pp, db, mock := setupPortalProxyWithAuthService(mockStratosAuth) + defer db.Close() + + // mock server + mockV2Info := setupMockServer(t, + msRoute("/v2/info"), + msMethod("GET"), + msStatus(http.StatusOK), + msBody(jsonMust(mockV2InfoResponse))) + + defer mockV2Info.Close() + + // temp impl to see if i broke something + req := setupMockReq("POST", "", map[string]string{ + "cnsi_name": "Some fancy CF Cluster", + "api_endpoint": mockV2Info.URL, + "skip_ssl_validation": "true", + "cnsi_client_id": mockClientId, + "cnsi_client_secret": mockClientSecret, + }) + + res := httptest.NewRecorder() + _, ctx := setupEchoContext(res, req) + + // Set a dummy userid in session - normally the login to UAA would do this. + sessionValues := make(map[string]interface{}) + sessionValues["user_id"] = mockAdminGUID + sessionValues["exp"] = time.Now().AddDate(0, 0, 1).Unix() + + if errSession := pp.setSessionValues(ctx, sessionValues); errSession != nil { + t.Error(errors.New("unable to mock/stub user in session object")) + } + + connectedUser := &interfaces.ConnectedUser{ + GUID: mockAdminGUID, + Admin: true, + } + + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockAdminGUID)). + Return(connectedUser, nil) + + pp.GetConfig().UserEndpointsEnabled = config.UserEndpointsConfigEnum.Enabled + //todo wieso funktioniert das nicht?? + rows := sqlmock.NewRows(rowFieldsForCNSI) + mock.ExpectQuery(selectAnyFromCNSIs).WithArgs(mockV2Info.URL).WillReturnRows(rows) + mock.ExpectQuery(selectAnyFromCNSIs).WithArgs(mockV2Info.URL).WillReturnRows(rows) + + mock.ExpectExec(insertIntoCNSIs). + WithArgs(sqlmock.AnyArg(), "Some fancy CF Cluster", "cf", mockV2Info.URL, mockAuthEndpoint, mockTokenEndpoint, mockDopplerEndpoint, true, mockClientId, sqlmock.AnyArg(), false, "", "", ""). + WillReturnResult(sqlmock.NewResult(1, 1)) + + if err := pp.RegisterEndpoint(ctx, getCFPlugin(pp, "cf").Info); err != nil { + t.Errorf("Failed to register cluster: %v", err) + } + + if dberr := mock.ExpectationsWereMet(); dberr != nil { + t.Errorf("There were unfulfilled expectations: %s", dberr) + } + }) +} diff --git a/src/jetstream/mock_server_test.go b/src/jetstream/mock_server_test.go index d6925c26c6..9c8161cc7a 100644 --- a/src/jetstream/mock_server_test.go +++ b/src/jetstream/mock_server_test.go @@ -214,6 +214,18 @@ func setupHTTPTest(req *http.Request) (*httptest.ResponseRecorder, *echo.Echo, e return res, e, ctx, pp, db, mock } +func setupPortalProxyWithAuthService(mockStratosAuth interfaces.StratosAuth) (*portalProxy, *sql.DB, sqlmock.Sqlmock) { + db, mock, dberr := sqlmock.New() + if dberr != nil { + fmt.Printf("an error '%s' was not expected when opening a stub database connection", dberr) + } + + pp := setupPortalProxy(db) + pp.StratosAuthService = mockStratosAuth + + return pp, db, mock +} + func msRoute(route string) mockServerFunc { return func(ms *mockServer) { ms.Route = route From b59304aa90ad5afbb92197e3d69dfbd86cfe4be1 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Tue, 23 Feb 2021 18:03:26 +0100 Subject: [PATCH 056/101] Clean up listCNSIByAPIEndpoint (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/cnsi.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 658c3dca94..564dfe1d4d 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -340,29 +340,28 @@ func (p *portalProxy) ListAdminEndpoints(userID string) ([]*interfaces.CNSIRecor return cnsiList, nil } -// listCNSIByAPIEndpoint - receives a URL as string (must be formatted like it's saved) +// listCNSIByAPIEndpoint - receives a URL as string func (p *portalProxy) listCNSIByAPIEndpoint(apiEndpoint string) ([]*interfaces.CNSIRecord, error) { log.Debug("listCNSIByAPIEndpoint") - var cnsiList []*interfaces.CNSIRecord var err error - - // Remove trailing slash, if there is one - apiEndpointURL, err := url.Parse(apiEndpoint) - if err != nil { - return nil, fmt.Errorf("Failed to get API Endpoint: %s", err) - } + cnsiList := []*interfaces.CNSIRecord{} cnsiRepo, err := p.GetStoreFactory().EndpointStore() if err != nil { return cnsiList, fmt.Errorf("listCNSIByAPIEndpoint: %s", err) } - cnsiList, err = cnsiRepo.ListByAPIEndpoint(fmt.Sprintf("%s", apiEndpointURL), p.Config.EncryptionKeyInBytes) + cnsiList, err = cnsiRepo.ListByAPIEndpoint(apiEndpoint, p.Config.EncryptionKeyInBytes) if err != nil { return cnsiList, err } + for _, cnsi := range cnsiList { + // Ensure that trailing slash is removed from the API Endpoint + cnsi.APIEndpoint.Path = strings.TrimRight(cnsi.APIEndpoint.Path, "/") + } + return cnsiList, nil } From 0bc452cced9eaba3a0498d3e6ec02b1dc402936c Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Wed, 24 Feb 2021 10:52:02 +0100 Subject: [PATCH 057/101] E2E test debug Signed-off-by: Thomas Quandt --- src/test-e2e/helpers/reset-helpers.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test-e2e/helpers/reset-helpers.ts b/src/test-e2e/helpers/reset-helpers.ts index 4f3579fc53..aa149aa960 100644 --- a/src/test-e2e/helpers/reset-helpers.ts +++ b/src/test-e2e/helpers/reset-helpers.ts @@ -136,8 +136,11 @@ export class ResetsHelpers { if (!data || !data.length) { return; } + console.warn('reset-helpers.ts: removeAllEndpoints: raw response', data); data = data.trim(); + console.warn('reset-helpers.ts: removeAllEndpoints: trimmed response', data); data = JSON.parse(data); + console.warn('reset-helpers.ts: removeAllEndpoints: json response', data); const p = promise.fulfilled({}); data.forEach((c) => { p.then(() => reqHelpers.sendRequest(req, { method: 'DELETE', url: 'api/v1/endpoints/' + c.guid }, null, {})); From 922e5a4ff2363958d44c1bb39fb099595d9a3c0e Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Wed, 24 Feb 2021 11:12:48 +0100 Subject: [PATCH 058/101] Optimize endpoints check during register (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/cnsi.go | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 564dfe1d4d..4f6959f119 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -126,18 +126,18 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk isAdmin = currentCreator.Admin } - // check if we've already got this endpoint in the DB - ok := p.adminCNSIRecordExists(apiEndpoint) - if ok { - // a record with the same api endpoint was found - return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( - http.StatusBadRequest, - "Can not register same endpoint multiple times", - "Can not register same endpoint multiple times", - ) - } - - if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled { + if p.GetConfig().UserEndpointsEnabled == config.UserEndpointsConfigEnum.Disabled { + // check if we've already got this endpoint in the DB + ok := p.adminCNSIRecordExists(apiEndpoint) + if ok { + // a record with the same api endpoint was found + return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( + http.StatusBadRequest, + "Can not register same endpoint multiple times", + "Can not register same endpoint multiple times", + ) + } + } else { // get all endpoints determined by the APIEndpoint duplicateEndpoints, err := p.listCNSIByAPIEndpoint(apiEndpoint) if err != nil { @@ -147,7 +147,6 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk "Failed to check other endpoints: %v", err) } - // check if we've already got this APIEndpoint in this DB for _, duplicate := range duplicateEndpoints { if len(duplicate.Creator) == 0 { From 9a2bc7ce8770e3c7c4e610235a00a35a65c8da72 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Wed, 24 Feb 2021 13:11:45 +0100 Subject: [PATCH 059/101] Revert "E2E test debug" Signed-off-by: Thomas Quandt --- src/test-e2e/helpers/reset-helpers.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/test-e2e/helpers/reset-helpers.ts b/src/test-e2e/helpers/reset-helpers.ts index aa149aa960..4f3579fc53 100644 --- a/src/test-e2e/helpers/reset-helpers.ts +++ b/src/test-e2e/helpers/reset-helpers.ts @@ -136,11 +136,8 @@ export class ResetsHelpers { if (!data || !data.length) { return; } - console.warn('reset-helpers.ts: removeAllEndpoints: raw response', data); data = data.trim(); - console.warn('reset-helpers.ts: removeAllEndpoints: trimmed response', data); data = JSON.parse(data); - console.warn('reset-helpers.ts: removeAllEndpoints: json response', data); const p = promise.fulfilled({}); data.forEach((c) => { p.then(() => reqHelpers.sendRequest(req, { method: 'DELETE', url: 'api/v1/endpoints/' + c.guid }, null, {})); From 2f2bf226ed4b64a365bd26ced145d3b8a905b677 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 25 Feb 2021 13:34:02 +0100 Subject: [PATCH 060/101] Fix overwriteEndpoint bug (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/cnsi.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 4f6959f119..1f400bcc25 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -149,6 +149,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk } // check if we've already got this APIEndpoint in this DB for _, duplicate := range duplicateEndpoints { + // cant create same admin endpoint if len(duplicate.Creator) == 0 { return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( http.StatusBadRequest, @@ -156,7 +157,10 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk "Can not register same admin endpoint multiple times", ) } - if duplicate.Creator == userId && !overwriteEndpoints { + + // cant create same user endpoint + // can create same user endpoint if overwriteEndpoint true + if duplicate.Creator == userId || !overwriteEndpoints { return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( http.StatusBadRequest, "Can not register same endpoint multiple times", @@ -165,12 +169,11 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk } } - if isAdmin { - // remove all endpoints with same APIEndpoint - for _, duplicate := range duplicateEndpoints { - p.doUnregisterCluster(duplicate.GUID) - } + // because of the check above, no admin endpoint or userendpoint without overwriteEndpoints=true will reach here + for _, duplicate := range duplicateEndpoints { + p.doUnregisterCluster(duplicate.GUID) } + } h := sha1.New() From 98364a0e4c7c309271708887dbdf7f80531c12ef Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 25 Feb 2021 15:43:59 +0100 Subject: [PATCH 061/101] Fix another overwriteEndpoint bug (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/cnsi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 1f400bcc25..242c3d533b 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -160,7 +160,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk // cant create same user endpoint // can create same user endpoint if overwriteEndpoint true - if duplicate.Creator == userId || !overwriteEndpoints { + if duplicate.Creator == userId || isAdmin && !overwriteEndpoints { return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( http.StatusBadRequest, "Can not register same endpoint multiple times", From 4ce5ab06c37046b4b3f7f24cfde1ec90bb681c65 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 25 Feb 2021 15:44:55 +0100 Subject: [PATCH 062/101] Add unit tests for registerEndpoint (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/cnsi_test.go | 345 +++++++++++++++++++++++++----- src/jetstream/mock_server_test.go | 63 ++++++ 2 files changed, 353 insertions(+), 55 deletions(-) diff --git a/src/jetstream/cnsi_test.go b/src/jetstream/cnsi_test.go index b2d68cfd45..64045235c9 100644 --- a/src/jetstream/cnsi_test.go +++ b/src/jetstream/cnsi_test.go @@ -274,7 +274,8 @@ func TestRegisterCFClusterWithUserEndpointsEnabled(t *testing.T) { // execute this in parallel t.Parallel() - Convey("Test a convey", t, func() { + Convey("Request to register endpoint", t, func() { + // mock StratosAuthService ctrl := gomock.NewController(t) mockStratosAuth := mock_interfaces.NewMockStratosAuth(ctrl) @@ -284,62 +285,296 @@ func TestRegisterCFClusterWithUserEndpointsEnabled(t *testing.T) { pp, db, mock := setupPortalProxyWithAuthService(mockStratosAuth) defer db.Close() - // mock server - mockV2Info := setupMockServer(t, - msRoute("/v2/info"), - msMethod("GET"), - msStatus(http.StatusOK), - msBody(jsonMust(mockV2InfoResponse))) - - defer mockV2Info.Close() - - // temp impl to see if i broke something - req := setupMockReq("POST", "", map[string]string{ - "cnsi_name": "Some fancy CF Cluster", - "api_endpoint": mockV2Info.URL, - "skip_ssl_validation": "true", - "cnsi_client_id": mockClientId, - "cnsi_client_secret": mockClientSecret, - }) - - res := httptest.NewRecorder() - _, ctx := setupEchoContext(res, req) - - // Set a dummy userid in session - normally the login to UAA would do this. - sessionValues := make(map[string]interface{}) - sessionValues["user_id"] = mockAdminGUID - sessionValues["exp"] = time.Now().AddDate(0, 0, 1).Unix() - - if errSession := pp.setSessionValues(ctx, sessionValues); errSession != nil { - t.Error(errors.New("unable to mock/stub user in session object")) - } - - connectedUser := &interfaces.ConnectedUser{ - GUID: mockAdminGUID, - Admin: true, + // mock individual APIEndpoints + mockV2Info := []*httptest.Server{} + for i := 0; i < 1; i++ { + server := setupMockServer(t, + msRoute("/v2/info"), + msMethod("GET"), + msStatus(http.StatusOK), + msBody(jsonMust(mockV2InfoResponse))) + defer server.Close() + mockV2Info = append(mockV2Info, server) } - mockStratosAuth. - EXPECT(). - GetUser(gomock.Eq(mockAdminGUID)). - Return(connectedUser, nil) - - pp.GetConfig().UserEndpointsEnabled = config.UserEndpointsConfigEnum.Enabled - //todo wieso funktioniert das nicht?? - rows := sqlmock.NewRows(rowFieldsForCNSI) - mock.ExpectQuery(selectAnyFromCNSIs).WithArgs(mockV2Info.URL).WillReturnRows(rows) - mock.ExpectQuery(selectAnyFromCNSIs).WithArgs(mockV2Info.URL).WillReturnRows(rows) - - mock.ExpectExec(insertIntoCNSIs). - WithArgs(sqlmock.AnyArg(), "Some fancy CF Cluster", "cf", mockV2Info.URL, mockAuthEndpoint, mockTokenEndpoint, mockDopplerEndpoint, true, mockClientId, sqlmock.AnyArg(), false, "", "", ""). - WillReturnResult(sqlmock.NewResult(1, 1)) - - if err := pp.RegisterEndpoint(ctx, getCFPlugin(pp, "cf").Info); err != nil { - t.Errorf("Failed to register cluster: %v", err) - } + // mock different users + mockAdmin := setupMockUser(mockAdminGUID, true, []string{}) + mockUser1 := setupMockUser(mockUserGUID+"1", false, []string{"stratos.endpointadmin"}) + mockUser2 := setupMockUser(mockUserGUID+"2", false, []string{"stratos.endpointadmin"}) + + Convey("with UserEndpointsEnabled=enabled", func() { + + pp.GetConfig().UserEndpointsEnabled = config.UserEndpointsConfigEnum.Enabled + + Convey("as admin", func() { + Convey("with overwrite disabled", func() { + // setup + adminEndpoint := setupMockEndpointRequest(t, mockAdmin.ConnectedUser, mockV2Info[0], "CF Cluster 1", false) + + if errSession := pp.setSessionValues(adminEndpoint.EchoContext, mockAdmin.SessionValues); errSession != nil { + t.Error(errors.New("unable to mock/stub user in session object")) + } + Convey("register new endpoint", func() { + // mock executions + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockAdmin.ConnectedUser.GUID)). + Return(mockAdmin.ConnectedUser, nil) + + // return no already saved endpoints + rows := sqlmock.NewRows(rowFieldsForCNSI) + mock.ExpectQuery(selectAnyFromCNSIs).WithArgs(mockV2Info[0].URL).WillReturnRows(rows) + + mock.ExpectExec(insertIntoCNSIs). + WithArgs(adminEndpoint.InsertArgs...). + WillReturnResult(sqlmock.NewResult(1, 1)) + + // test + err := pp.RegisterEndpoint(adminEndpoint.EchoContext, getCFPlugin(pp, "cf").Info) + dberr := mock.ExpectationsWereMet() + + Convey("there should be no error", func() { + So(err, ShouldBeNil) + }) + + Convey("there should be no db error", func() { + So(dberr, ShouldBeNil) + }) + }) + Convey("overwrite existing user endpoints", func() { + // setup + userEndpoint := setupMockEndpointRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1 User", false) + + // mock executions + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockAdmin.ConnectedUser.GUID)). + Return(mockAdmin.ConnectedUser, nil) + + // return a user endpoint with same apiurl + rows := sqlmock.NewRows(rowFieldsForCNSI).AddRow(userEndpoint.QueryArgs...) + mock.ExpectQuery(selectAnyFromCNSIs).WithArgs(mockV2Info[0].URL).WillReturnRows(rows) + + // test + err := pp.RegisterEndpoint(adminEndpoint.EchoContext, getCFPlugin(pp, "cf").Info) + dberr := mock.ExpectationsWereMet() + + Convey("there should be no error", func() { + So(err, ShouldResemble, interfaces.NewHTTPShadowError( + http.StatusBadRequest, + "Can not register same endpoint multiple times", + "Can not register same endpoint multiple times", + )) + }) + + Convey("there should be no db error", func() { + So(dberr, ShouldBeNil) + }) + }) + }) + Convey("with overwrite enabled", func() { + + // setup + adminEndpoint := setupMockEndpointRequest(t, mockAdmin.ConnectedUser, mockV2Info[0], "CF Cluster 1", true) + + if errSession := pp.setSessionValues(adminEndpoint.EchoContext, mockAdmin.SessionValues); errSession != nil { + t.Error(errors.New("unable to mock/stub user in session object")) + } + + Convey("overwrite existing admin endpoints", func() { + // mock executions + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockAdmin.ConnectedUser.GUID)). + Return(mockAdmin.ConnectedUser, nil) + + // return a admin endpoint with same apiurl + rows := sqlmock.NewRows(rowFieldsForCNSI).AddRow(adminEndpoint.QueryArgs...) + mock.ExpectQuery(selectAnyFromCNSIs).WithArgs(mockV2Info[0].URL).WillReturnRows(rows) + + // test + err := pp.RegisterEndpoint(adminEndpoint.EchoContext, getCFPlugin(pp, "cf").Info) + dberr := mock.ExpectationsWereMet() + + Convey("should fail ", func() { + So(err, ShouldResemble, interfaces.NewHTTPShadowError( + http.StatusBadRequest, + "Can not register same admin endpoint multiple times", + "Can not register same admin endpoint multiple times", + )) + }) + + Convey("no insert should be executed", func() { + So(dberr, ShouldBeNil) + }) + }) + Convey("overwrite existing user endpoints", func() { + + // setup + userEndpoint := setupMockEndpointRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1 User", false) + + // mock executions + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockAdmin.ConnectedUser.GUID)). + Return(mockAdmin.ConnectedUser, nil) + + // return a user endpoint with same apiurl + rows := sqlmock.NewRows(rowFieldsForCNSI).AddRow(userEndpoint.QueryArgs...) + mock.ExpectQuery(selectAnyFromCNSIs).WithArgs(mockV2Info[0].URL).WillReturnRows(rows) + + // user endpoints should be deleted + mock.ExpectExec(deleteFromCNSIs). + WithArgs(userEndpoint.QueryArgs[0]). + WillReturnResult(sqlmock.NewResult(1, 1)) + + // a new admin endpoint with same url will be registered + mock.ExpectExec(insertIntoCNSIs). + WithArgs(adminEndpoint.InsertArgs...). + WillReturnResult(sqlmock.NewResult(1, 1)) + + // test + err := pp.RegisterEndpoint(adminEndpoint.EchoContext, getCFPlugin(pp, "cf").Info) + dberr := mock.ExpectationsWereMet() + + Convey("there should be no error", func() { + So(err, ShouldBeNil) + }) + + Convey("there should be no db error", func() { + So(dberr, ShouldBeNil) + }) + }) + }) + }) + + Convey("as user", func() { + Convey("with overwrite disabled", func() { + // setup + userEndpoint := setupMockEndpointRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1", false) + + if errSession := pp.setSessionValues(userEndpoint.EchoContext, mockUser1.SessionValues); errSession != nil { + t.Error(errors.New("unable to mock/stub user in session object")) + } + + Convey("register new endpoint", func() { + // mock executions + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockUser1.ConnectedUser.GUID)). + Return(mockUser1.ConnectedUser, nil) + + rows := sqlmock.NewRows(rowFieldsForCNSI) + mock.ExpectQuery(selectAnyFromCNSIs).WithArgs(mockV2Info[0].URL).WillReturnRows(rows) + + mock.ExpectExec(insertIntoCNSIs). + WithArgs(userEndpoint.InsertArgs...). + WillReturnResult(sqlmock.NewResult(1, 1)) + + err := pp.RegisterEndpoint(userEndpoint.EchoContext, getCFPlugin(pp, "cf").Info) + dberr := mock.ExpectationsWereMet() + + Convey("there should be no error", func() { + So(err, ShouldBeNil) + }) + + Convey("there should be no db error", func() { + So(dberr, ShouldBeNil) + }) + }) + Convey("register existing endpoint from different user", func() { + userEndpoint2 := setupMockEndpointRequest(t, mockUser2.ConnectedUser, mockV2Info[0], "CF Cluster 2", false) + + // mock executions + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockUser1.ConnectedUser.GUID)). + Return(mockUser1.ConnectedUser, nil) + + rows := sqlmock.NewRows(rowFieldsForCNSI).AddRow(userEndpoint2.QueryArgs...) + mock.ExpectQuery(selectAnyFromCNSIs).WithArgs(mockV2Info[0].URL).WillReturnRows(rows) + + mock.ExpectExec(insertIntoCNSIs). + WithArgs(userEndpoint.InsertArgs...). + WillReturnResult(sqlmock.NewResult(1, 1)) + + err := pp.RegisterEndpoint(userEndpoint.EchoContext, getCFPlugin(pp, "cf").Info) + dberr := mock.ExpectationsWereMet() + + Convey("there should be no error", func() { + So(err, ShouldBeNil) + }) + + Convey("there should be no db error", func() { + So(dberr, ShouldBeNil) + }) + }) + Convey("register existing endpoint from same user", func() { + // mock executions + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockUser1.ConnectedUser.GUID)). + Return(mockUser1.ConnectedUser, nil) + + rows := sqlmock.NewRows(rowFieldsForCNSI).AddRow(userEndpoint.QueryArgs...) + mock.ExpectQuery(selectAnyFromCNSIs).WithArgs(mockV2Info[0].URL).WillReturnRows(rows) + + err := pp.RegisterEndpoint(userEndpoint.EchoContext, getCFPlugin(pp, "cf").Info) + dberr := mock.ExpectationsWereMet() + + Convey("should fail ", func() { + So(err, ShouldResemble, interfaces.NewHTTPShadowError( + http.StatusBadRequest, + "Can not register same endpoint multiple times", + "Can not register same endpoint multiple times", + )) + }) + + Convey("there should be no db error", func() { + So(dberr, ShouldBeNil) + }) + }) + }) + Convey("with overwrite enabled", func() { + userEndpoint := setupMockEndpointRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1", false) + + if errSession := pp.setSessionValues(userEndpoint.EchoContext, mockUser1.SessionValues); errSession != nil { + t.Error(errors.New("unable to mock/stub user in session object")) + } + Convey("overwrite existing endpoints from same user, with overwrite enabled", func() { + // mock executions + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockUser1.ConnectedUser.GUID)). + Return(mockUser1.ConnectedUser, nil) + + rows := sqlmock.NewRows(rowFieldsForCNSI).AddRow(userEndpoint.QueryArgs...) + mock.ExpectQuery(selectAnyFromCNSIs).WithArgs(mockV2Info[0].URL).WillReturnRows(rows) + + err := pp.RegisterEndpoint(userEndpoint.EchoContext, getCFPlugin(pp, "cf").Info) + dberr := mock.ExpectationsWereMet() + + Convey("should fail ", func() { + So(err, ShouldResemble, interfaces.NewHTTPShadowError( + http.StatusBadRequest, + "Can not register same endpoint multiple times", + "Can not register same endpoint multiple times", + )) + }) + + Convey("there should be no db error", func() { + So(dberr, ShouldBeNil) + }) + }) + }) + + }) + }) - if dberr := mock.ExpectationsWereMet(); dberr != nil { - t.Errorf("There were unfulfilled expectations: %s", dberr) - } + Convey("with UserEndpointsEnabled=disabled", func() { + Convey("register existing user endpoint", func() { + // todo user endpoints should automatically overwritten? + }) + }) }) } diff --git a/src/jetstream/mock_server_test.go b/src/jetstream/mock_server_test.go index 9c8161cc7a..58c430d600 100644 --- a/src/jetstream/mock_server_test.go +++ b/src/jetstream/mock_server_test.go @@ -1,11 +1,15 @@ package main import ( + "crypto/sha1" "database/sql" + "database/sql/driver" + "encoding/base64" "fmt" "net/http" "net/http/httptest" "net/url" + "strconv" "strings" "testing" "time" @@ -39,6 +43,19 @@ type mockPGStore struct { StoredSession *sessions.Session } +type MockEndpointRequest struct { + HTTPTestServer *httptest.Server + EchoContext echo.Context + EndpointName string + InsertArgs []driver.Value + QueryArgs []driver.Value +} + +type MockUser struct { + ConnectedUser *interfaces.ConnectedUser + SessionValues map[string]interface{} +} + func (m *mockPGStore) New(r *http.Request, name string) (*sessions.Session, error) { session := &sessions.Session{ Values: make(map[interface{}]interface{}), @@ -226,6 +243,51 @@ func setupPortalProxyWithAuthService(mockStratosAuth interfaces.StratosAuth) (*p return pp, db, mock } +func setupMockUser(guid string, admin bool, scopes []string) MockUser { + mockUser := MockUser{nil, nil} + mockUser.ConnectedUser = &interfaces.ConnectedUser{ + GUID: guid, + Admin: admin, + Scopes: scopes, + } + mockUser.SessionValues = make(map[string]interface{}) + mockUser.SessionValues["user_id"] = guid + mockUser.SessionValues["exp"] = time.Now().AddDate(0, 0, 1).Unix() + + return mockUser +} + +// mockV2Info needs to be closed +func setupMockEndpointRequest(t *testing.T, user *interfaces.ConnectedUser, mockV2Info *httptest.Server, endpointName string, overwriteEndpoints bool) MockEndpointRequest { + + // create a request for each endpoint + req := setupMockReq("POST", "", map[string]string{ + "cnsi_name": endpointName, + "api_endpoint": mockV2Info.URL, + "skip_ssl_validation": "true", + "cnsi_client_id": mockClientId, + "cnsi_client_secret": mockClientSecret, + "overwrite_endpoints": strconv.FormatBool(overwriteEndpoints), + }) + + res := httptest.NewRecorder() + _, ctx := setupEchoContext(res, req) + + uaaUserGUID := "" + + h := sha1.New() + if user.Admin { + h.Write([]byte(mockV2Info.URL)) + } else { + h.Write([]byte(mockV2Info.URL + user.GUID)) + uaaUserGUID = user.GUID + } + insertArgs := []driver.Value{base64.RawURLEncoding.EncodeToString(h.Sum(nil)), endpointName, "cf", mockV2Info.URL, mockAuthEndpoint, mockTokenEndpoint, mockDopplerEndpoint, true, mockClientId, sqlmock.AnyArg(), false, "", "", uaaUserGUID} + queryArgs := []driver.Value{base64.RawURLEncoding.EncodeToString(h.Sum(nil)), endpointName, "cf", mockV2Info.URL, mockAuthEndpoint, mockTokenEndpoint, mockDopplerEndpoint, true, mockClientId, cipherClientSecret, false, "", "", uaaUserGUID} + + return MockEndpointRequest{mockV2Info, ctx, endpointName, insertArgs, queryArgs} +} + func msRoute(route string) mockServerFunc { return func(ms *mockServer) { ms.Route = route @@ -302,6 +364,7 @@ const ( insertIntoTokens = `INSERT INTO tokens` updateTokens = `UPDATE tokens` selectAnyFromCNSIs = `SELECT (.+) FROM cnsis WHERE (.+)` + deleteFromCNSIs = `DELETE FROM cnsis WHERE (.+)` insertIntoCNSIs = `INSERT INTO cnsis` findUserGUID = `SELECT user_guid FROM local_users WHERE (.+)` addLocalUser = `INSERT INTO local_users (.+)` From fa45fe329e9a84d22c26531d3ae0ac6bd7b7a6a6 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 25 Feb 2021 15:56:08 +0100 Subject: [PATCH 063/101] Add another check for overwriteEndpoints (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/cnsi.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 242c3d533b..2cd92d0763 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -169,9 +169,10 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk } } - // because of the check above, no admin endpoint or userendpoint without overwriteEndpoints=true will reach here - for _, duplicate := range duplicateEndpoints { - p.doUnregisterCluster(duplicate.GUID) + if isAdmin && overwriteEndpoints { + for _, duplicate := range duplicateEndpoints { + p.doUnregisterCluster(duplicate.GUID) + } } } From 8b4fd83f0999d8aa324cf890ae2ab398e5e6ca72 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 25 Feb 2021 17:45:37 +0100 Subject: [PATCH 064/101] Add unit tests for listCNSI (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/cnsi_test.go | 178 +++++++++++++++++++++++++++++- src/jetstream/mock_server_test.go | 42 ++++--- 2 files changed, 206 insertions(+), 14 deletions(-) diff --git a/src/jetstream/cnsi_test.go b/src/jetstream/cnsi_test.go index 64045235c9..c7b1f425f0 100644 --- a/src/jetstream/cnsi_test.go +++ b/src/jetstream/cnsi_test.go @@ -270,7 +270,7 @@ func TestGetCFv2InfoWithInvalidEndpoint(t *testing.T) { } } -func TestRegisterCFClusterWithUserEndpointsEnabled(t *testing.T) { +func TestRegisterWithUserEndpointsEnabled(t *testing.T) { // execute this in parallel t.Parallel() @@ -578,3 +578,179 @@ func TestRegisterCFClusterWithUserEndpointsEnabled(t *testing.T) { }) }) } + +func TestListCNSIsWithUserEndpointsEnabled(t *testing.T) { + t.Parallel() + + Convey("Request to list endpoints", t, func() { + + // mock StratosAuthService + ctrl := gomock.NewController(t) + mockStratosAuth := mock_interfaces.NewMockStratosAuth(ctrl) + defer ctrl.Finish() + + // setup mock DB, PortalProxy and mock StratosAuthService + pp, db, mock := setupPortalProxyWithAuthService(mockStratosAuth) + defer db.Close() + + // setup request + + res := httptest.NewRecorder() + req := setupMockReq("GET", "", nil) + _, ctx := setupEchoContext(res, req) + + mockAdmin := setupMockUser(mockAdminGUID, true, []string{}) + mockUser1 := setupMockUser(mockUserGUID+"1", false, []string{"stratos.endpointadmin"}) + mockUser2 := setupMockUser(mockUserGUID+"2", false, []string{"stratos.endpointadmin"}) + + adminEndpointArgs := createEndpointRowArgs("CF Endpoint 1", "https://127.0.0.1:50001", mockAdmin.ConnectedUser.GUID, mockAdmin.ConnectedUser.Admin) + userEndpoint1Args := createEndpointRowArgs("CF Endpoint 2", "https://127.0.0.1:50002", mockUser1.ConnectedUser.GUID, mockUser1.ConnectedUser.Admin) + userEndpoint2Args := createEndpointRowArgs("CF Endpoint 3", "https://127.0.0.1:50003", mockUser2.ConnectedUser.GUID, mockUser2.ConnectedUser.Admin) + + adminRows := sqlmock.NewRows(rowFieldsForCNSI). + AddRow(adminEndpointArgs...) + user1Rows := sqlmock.NewRows(rowFieldsForCNSI). + AddRow(userEndpoint1Args...) + allRows := sqlmock.NewRows(rowFieldsForCNSI). + AddRow(adminEndpointArgs...). + AddRow(userEndpoint1Args...). + AddRow(userEndpoint2Args...) + + Convey("as admin", func() { + + if errSession := pp.setSessionValues(ctx, mockAdmin.SessionValues); errSession != nil { + t.Error(errors.New("unable to mock/stub user in session object")) + } + + Convey("with UserEndpointsEnabled = enabled", func() { + //expect list all + pp.GetConfig().UserEndpointsEnabled = config.UserEndpointsConfigEnum.Enabled + + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockAdmin.ConnectedUser.GUID)). + Return(mockAdmin.ConnectedUser, nil) + + mock.ExpectQuery(selectFromCNSIs).WillReturnRows(allRows) + err := pp.listCNSIs(ctx) + dberr := mock.ExpectationsWereMet() + + Convey("there should be no error", func() { + So(err, ShouldBeNil) + }) + + Convey("there should be no db error", func() { + So(dberr, ShouldBeNil) + }) + }) + Convey("with UserEndpointsEnabled = admin_only", func() { + //expect list all + pp.GetConfig().UserEndpointsEnabled = config.UserEndpointsConfigEnum.AdminOnly + + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockAdmin.ConnectedUser.GUID)). + Return(mockAdmin.ConnectedUser, nil) + + mock.ExpectQuery(selectFromCNSIs).WillReturnRows(allRows) + err := pp.listCNSIs(ctx) + dberr := mock.ExpectationsWereMet() + + Convey("there should be no error", func() { + So(err, ShouldBeNil) + }) + + Convey("there should be no db error", func() { + So(dberr, ShouldBeNil) + }) + + }) + Convey("with UserEndpointsEnabled = disabled", func() { + // expect list creator with "" + pp.GetConfig().UserEndpointsEnabled = config.UserEndpointsConfigEnum.Disabled + + mock.ExpectQuery(selectCreatorFromCNSIs).WithArgs("").WillReturnRows(adminRows) + err := pp.listCNSIs(ctx) + dberr := mock.ExpectationsWereMet() + + Convey("there should be no error", func() { + So(err, ShouldBeNil) + }) + + Convey("there should be no db error", func() { + So(dberr, ShouldBeNil) + }) + }) + + }) + Convey("as user", func() { + //expect list creator with "" and user-guid as args + if errSession := pp.setSessionValues(ctx, mockUser1.SessionValues); errSession != nil { + t.Error(errors.New("unable to mock/stub user in session object")) + } + + Convey("with UserEndpointsEnabled = enabled", func() { + // expect list creator with "" and own endpoints + pp.GetConfig().UserEndpointsEnabled = config.UserEndpointsConfigEnum.Enabled + + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockUser1.ConnectedUser.GUID)). + Return(mockUser1.ConnectedUser, nil) + + mock.ExpectQuery(selectCreatorFromCNSIs).WithArgs(mockUser1.ConnectedUser.GUID).WillReturnRows(user1Rows) + mock.ExpectQuery(selectCreatorFromCNSIs).WithArgs("").WillReturnRows(adminRows) + err := pp.listCNSIs(ctx) + dberr := mock.ExpectationsWereMet() + + Convey("there should be no error", func() { + So(err, ShouldBeNil) + }) + + Convey("there should be no db error", func() { + So(dberr, ShouldBeNil) + }) + + }) + Convey("with UserEndpointsEnabled = admin_only", func() { + // expect list creator with "" + pp.GetConfig().UserEndpointsEnabled = config.UserEndpointsConfigEnum.AdminOnly + + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockUser1.ConnectedUser.GUID)). + Return(mockUser1.ConnectedUser, nil) + + mock.ExpectQuery(selectCreatorFromCNSIs).WithArgs("").WillReturnRows(adminRows) + err := pp.listCNSIs(ctx) + dberr := mock.ExpectationsWereMet() + + Convey("there should be no error", func() { + So(err, ShouldBeNil) + }) + + Convey("there should be no db error", func() { + So(dberr, ShouldBeNil) + }) + + }) + Convey("with UserEndpointsEnabled = disabled", func() { + // expect list creator with "" + pp.GetConfig().UserEndpointsEnabled = config.UserEndpointsConfigEnum.Disabled + + mock.ExpectQuery(selectCreatorFromCNSIs).WithArgs("").WillReturnRows(adminRows) + err := pp.listCNSIs(ctx) + dberr := mock.ExpectationsWereMet() + + Convey("there should be no error", func() { + So(err, ShouldBeNil) + }) + + Convey("there should be no db error", func() { + So(dberr, ShouldBeNil) + }) + }) + + }) + }) +} diff --git a/src/jetstream/mock_server_test.go b/src/jetstream/mock_server_test.go index 58c430d600..4581bebf0c 100644 --- a/src/jetstream/mock_server_test.go +++ b/src/jetstream/mock_server_test.go @@ -219,6 +219,20 @@ func expectEncryptedTokenRow(mockEncryptionKey []byte) sqlmock.Rows { AddRow(mockTokenGUID, encryptedUaaToken, encryptedUaaToken, mockTokenExpiry, false, "OAuth2", "", mockUserGUID, nil) } +func createEndpointRowArgs(endpointName string, APIEndpoint string, uaaUserGUID string, userAdmin bool) []driver.Value { + creatorGUID := "" + + h := sha1.New() + if userAdmin { + h.Write([]byte(APIEndpoint)) + } else { + h.Write([]byte(APIEndpoint + uaaUserGUID)) + creatorGUID = uaaUserGUID + } + + return []driver.Value{base64.RawURLEncoding.EncodeToString(h.Sum(nil)), endpointName, "cf", APIEndpoint, mockAuthEndpoint, mockTokenEndpoint, mockDopplerEndpoint, true, mockClientId, cipherClientSecret, false, "", "", creatorGUID} +} + func setupHTTPTest(req *http.Request) (*httptest.ResponseRecorder, *echo.Echo, echo.Context, *portalProxy, *sql.DB, sqlmock.Sqlmock) { res := httptest.NewRecorder() e, ctx := setupEchoContext(res, req) @@ -360,19 +374,21 @@ const ( stringCFType = "cf" - selectAnyFromTokens = `SELECT (.+) FROM tokens WHERE (.+)` - insertIntoTokens = `INSERT INTO tokens` - updateTokens = `UPDATE tokens` - selectAnyFromCNSIs = `SELECT (.+) FROM cnsis WHERE (.+)` - deleteFromCNSIs = `DELETE FROM cnsis WHERE (.+)` - insertIntoCNSIs = `INSERT INTO cnsis` - findUserGUID = `SELECT user_guid FROM local_users WHERE (.+)` - addLocalUser = `INSERT INTO local_users (.+)` - findPasswordHash = `SELECT password_hash FROM local_users WHERE (.+)` - findUserScope = `SELECT user_scope FROM local_users WHERE (.+)` - updateLastLoginTime = `UPDATE local_users (.+)` - findLastLoginTime = `SELECT last_login FROM local_users WHERE (.+)` - getDbVersion = `SELECT version_id FROM goose_db_version WHERE is_applied = '1' ORDER BY id DESC LIMIT 1` + selectAnyFromTokens = `SELECT (.+) FROM tokens WHERE (.+)` + insertIntoTokens = `INSERT INTO tokens` + updateTokens = `UPDATE tokens` + selectFromCNSIs = `SELECT (.+) FROM cnsis` + selectAnyFromCNSIs = `SELECT (.+) FROM cnsis WHERE (.+)` + selectCreatorFromCNSIs = `SELECT (.+) FROM cnsis WHERE creator=(.+)` + deleteFromCNSIs = `DELETE FROM cnsis WHERE (.+)` + insertIntoCNSIs = `INSERT INTO cnsis` + findUserGUID = `SELECT user_guid FROM local_users WHERE (.+)` + addLocalUser = `INSERT INTO local_users (.+)` + findPasswordHash = `SELECT password_hash FROM local_users WHERE (.+)` + findUserScope = `SELECT user_scope FROM local_users WHERE (.+)` + updateLastLoginTime = `UPDATE local_users (.+)` + findLastLoginTime = `SELECT last_login FROM local_users WHERE (.+)` + getDbVersion = `SELECT version_id FROM goose_db_version WHERE is_applied = '1' ORDER BY id DESC LIMIT 1` ) var rowFieldsForCNSI = []string{"guid", "name", "cnsi_type", "api_endpoint", "auth_endpoint", "token_endpoint", "doppler_logging_endpoint", "skip_ssl_validation", "client_id", "client_secret", "allow_sso", "sub_type", "meta_data", "creator"} From 1cc80214e4103a5a3d05cda8f584c70b28a9e2bb Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 25 Feb 2021 17:48:51 +0100 Subject: [PATCH 065/101] Remove comment (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/cnsi_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/src/jetstream/cnsi_test.go b/src/jetstream/cnsi_test.go index c7b1f425f0..9d501f7468 100644 --- a/src/jetstream/cnsi_test.go +++ b/src/jetstream/cnsi_test.go @@ -684,7 +684,6 @@ func TestListCNSIsWithUserEndpointsEnabled(t *testing.T) { }) Convey("as user", func() { - //expect list creator with "" and user-guid as args if errSession := pp.setSessionValues(ctx, mockUser1.SessionValues); errSession != nil { t.Error(errors.New("unable to mock/stub user in session object")) } From a381d004a138ebf7b1b7695289663582dcd2cf01 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Fri, 26 Feb 2021 14:28:47 +0100 Subject: [PATCH 066/101] Create superclass for existing endpoints check (#4876) Signed-off-by: Thomas Quandt --- .../create-endpoint-cf-step-1.component.ts | 16 ++----- .../create-endpoint-helper.component.spec.ts | 32 +++++++++++++ .../create-endpoint-helper.component.ts | 46 +++++++++++++++++++ .../create-endpoint/create-endpoint.module.ts | 7 ++- .../git-registration.component.ts | 35 ++------------ 5 files changed, 92 insertions(+), 44 deletions(-) create mode 100644 src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper/create-endpoint-helper.component.spec.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper/create-endpoint-helper.component.ts diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts index 8c14f951da..e924b43185 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts @@ -18,18 +18,15 @@ import { SnackBarService } from '../../../../shared/services/snackbar.service'; import { ConnectEndpointConfig } from '../../connect.service'; import { getSSOClientRedirectURI } from '../../endpoint-helpers'; import { SessionService } from '../../../../shared/services/session.service'; +import { CreateEndpointHelperComponent } from '../create-endpoint-helper/create-endpoint-helper.component'; -type EndpointObservable = Observable<{ - names: string[], - urls: string[], -}>; @Component({ selector: 'app-create-endpoint-cf-step-1', templateUrl: './create-endpoint-cf-step-1.component.html', styleUrls: ['./create-endpoint-cf-step-1.component.scss'] }) -export class CreateEndpointCfStep1Component implements IStepperStep, AfterContentInit { +export class CreateEndpointCfStep1Component extends CreateEndpointHelperComponent implements IStepperStep, AfterContentInit { registerForm: FormGroup; @@ -49,11 +46,6 @@ export class CreateEndpointCfStep1Component implements IStepperStep, AfterConten } } - overwritePermission: Observable; - - existingEndpoints: EndpointObservable; - existingAdminEndpoints: EndpointObservable; - validate: Observable; urlValidation: string; @@ -71,8 +63,10 @@ export class CreateEndpointCfStep1Component implements IStepperStep, AfterConten private fb: FormBuilder, activatedRoute: ActivatedRoute, private snackBarService: SnackBarService, - private sessionService: SessionService + sessionService: SessionService ) { + super(sessionService); + this.registerForm = this.fb.group({ nameField: ['', [Validators.required]], urlField: ['', [Validators.required]], diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper/create-endpoint-helper.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper/create-endpoint-helper.component.spec.ts new file mode 100644 index 0000000000..7ea0d22cbb --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper/create-endpoint-helper.component.spec.ts @@ -0,0 +1,32 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SessionService } from 'frontend/packages/core/src/shared/services/session.service'; +import { createBasicStoreModule } from '@stratosui/store/testing'; +import { CreateEndpointHelperComponent } from './create-endpoint-helper.component'; +import { CoreTestingModule } from '../../../../../test-framework/core-test.modules'; + +describe('CreateEndpointHelperComponent', () => { + let component: CreateEndpointHelperComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ CreateEndpointHelperComponent ], + imports: [ + CoreTestingModule, + createBasicStoreModule(), + ], + providers: [ SessionService ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CreateEndpointHelperComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper/create-endpoint-helper.component.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper/create-endpoint-helper.component.ts new file mode 100644 index 0000000000..08551dfbe5 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper/create-endpoint-helper.component.ts @@ -0,0 +1,46 @@ +import { Component } from '@angular/core'; +import { StratosCurrentUserPermissions } from '../../../../core/permissions/stratos-user-permissions.checker'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { getFullEndpointApiUrl } from '../../../../../../store/src/endpoint-utils'; +import { stratosEntityCatalog } from '../../../../../../store/src/stratos-entity-catalog'; +import { SessionService } from '../../../../shared/services/session.service'; + +type EndpointObservable = Observable<{ + names: string[], + urls: string[], +}>; + +@Component({ + selector: 'app-create-endpoint-helper', + template: '' +}) +export class CreateEndpointHelperComponent { + + overwritePermission: Observable; + existingEndpoints: EndpointObservable; + existingAdminEndpoints: EndpointObservable; + + constructor( + public sessionService: SessionService + ) { + const currentPage$ = stratosEntityCatalog.endpoint.store.getAll.getPaginationMonitor().currentPage$; + this.existingAdminEndpoints = currentPage$.pipe( + map(endpoints => ({ + names: endpoints.filter(ep => ep.creator.admin).map(ep => ep.name), + urls: endpoints.filter(ep => ep.creator.admin).map(ep => getFullEndpointApiUrl(ep)), + })) + ); + this.existingEndpoints = currentPage$.pipe( + map(endpoints => ({ + names: endpoints.map(ep => ep.name), + urls: endpoints.map(ep => getFullEndpointApiUrl(ep)), + })) + ); + + this.overwritePermission = this.sessionService.userEndpointsNotDisabled().pipe( + map(enabled => enabled ? [StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT] : []) + ); + } +} diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.module.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.module.ts index e00c1a62cb..1505415ee2 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.module.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.module.ts @@ -8,6 +8,7 @@ import { CreateEndpointBaseStepComponent } from './create-endpoint-base-step/cre import { CreateEndpointCfStep1Component } from './create-endpoint-cf-step-1/create-endpoint-cf-step-1.component'; import { CreateEndpointConnectComponent } from './create-endpoint-connect/create-endpoint-connect.component'; import { CreateEndpointComponent } from './create-endpoint.component'; +import { CreateEndpointHelperComponent } from './create-endpoint-helper/create-endpoint-helper.component'; @NgModule({ imports: [ @@ -20,13 +21,15 @@ import { CreateEndpointComponent } from './create-endpoint.component'; CreateEndpointCfStep1Component, CreateEndpointBaseStepComponent, CreateEndpointConnectComponent, - ConnectEndpointComponent + ConnectEndpointComponent, + CreateEndpointHelperComponent ], exports: [ CreateEndpointComponent, CreateEndpointCfStep1Component, CreateEndpointConnectComponent, - ConnectEndpointComponent + ConnectEndpointComponent, + CreateEndpointHelperComponent ] }) export class CreateEndpointModule { } diff --git a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts index 9bde31b181..ddfb95dcf9 100644 --- a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts +++ b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts @@ -4,7 +4,6 @@ import { ActivatedRoute } from '@angular/router'; import { Observable, Subscription } from 'rxjs'; import { filter, first, map, pairwise } from 'rxjs/operators'; -import { StratosCurrentUserPermissions } from '../../../../../core/src/core/permissions/stratos-user-permissions.checker'; import { SessionService } from '../../../../../core/src/shared/services/session.service'; import { EndpointsService } from '../../../../../core/src/core/endpoints.service'; import { getIdFromRoute } from '../../../../../core/src/core/utils.service'; @@ -17,6 +16,7 @@ import { ActionState } from '../../../../../store/src/reducers/api-request-reduc import { stratosEntityCatalog } from '../../../../../store/src/stratos-entity-catalog'; import { GIT_ENDPOINT_SUB_TYPES, GIT_ENDPOINT_TYPE } from '../../../store/git-entity-factory'; import { GitSCMService } from '../../scm/scm.service'; +import { CreateEndpointHelperComponent } from 'frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper/create-endpoint-helper.component'; interface EndpointSubTypes { [subType: string]: GithubTypes; @@ -46,17 +46,12 @@ enum GitTypeKeys { GITLAB_ENTERPRISE = 'githubenterprize', } -type EndpointObservable = Observable<{ - names: string[], - urls: string[], -}>; - @Component({ selector: 'app-git-registration', templateUrl: './git-registration.component.html', styleUrls: ['./git-registration.component.scss'], }) -export class GitRegistrationComponent implements OnDestroy { +export class GitRegistrationComponent extends CreateEndpointHelperComponent implements OnDestroy { public gitTypes: EndpointSubTypes; @@ -72,19 +67,15 @@ export class GitRegistrationComponent implements OnDestroy { urlValidation: string; - overwritePermission: Observable; - - existingEndpoints: EndpointObservable; - existingAdminEndpoints: EndpointObservable; - constructor( gitSCMService: GitSCMService, activatedRoute: ActivatedRoute, private fb: FormBuilder, private snackBarService: SnackBarService, private endpointsService: EndpointsService, - private sessionService: SessionService, + public sessionService: SessionService, ) { + super(sessionService); this.epSubType = getIdFromRoute(activatedRoute, 'subtype'); const githubLabel = entityCatalog.getEndpoint(GIT_ENDPOINT_TYPE, GIT_ENDPOINT_SUB_TYPES.GITHUB).definition.label || 'Github'; const gitlabLabel = entityCatalog.getEndpoint(GIT_ENDPOINT_TYPE, GIT_ENDPOINT_SUB_TYPES.GITLAB).definition.label || 'Gitlab'; @@ -143,24 +134,6 @@ export class GitRegistrationComponent implements OnDestroy { } }; - const currentPage$ = stratosEntityCatalog.endpoint.store.getAll.getPaginationMonitor().currentPage$; - this.existingAdminEndpoints = currentPage$.pipe( - map(endpoints => ({ - names: endpoints.filter(ep => ep.creator.admin).map(ep => ep.name), - urls: endpoints.filter(ep => ep.creator.admin).map(ep => getFullEndpointApiUrl(ep)), - })) - ); - this.existingEndpoints = currentPage$.pipe( - map(endpoints => ({ - names: endpoints.map(ep => ep.name), - urls: endpoints.map(ep => getFullEndpointApiUrl(ep)), - })) - ); - - this.overwritePermission = this.sessionService.userEndpointsNotDisabled().pipe( - map(enabled => enabled ? [StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT] : []) - ); - // Check the endpoints and turn off any options for endpoints that are already registered this.endpointsService.endpoints$.pipe(first()).subscribe(eps => { Object.values(this.gitTypes[this.epSubType].types).forEach(type => { From 20419649ad43ec6cff4505566e16037f123902ad Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Fri, 26 Feb 2021 14:31:19 +0100 Subject: [PATCH 067/101] React to err for doUnregisterCluster (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/cnsi.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 2cd92d0763..bc1511a1ad 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -171,7 +171,14 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk if isAdmin && overwriteEndpoints { for _, duplicate := range duplicateEndpoints { - p.doUnregisterCluster(duplicate.GUID) + err := p.doUnregisterCluster(duplicate.GUID) + if err != nil { + return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( + http.StatusInternalServerError, + "Failed to unregister cluster", + "Failed to unregister cluster: %v", + err) + } } } From 48c2e4fb7c77d1698d924c2960d6141e34c029c0 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Mon, 1 Mar 2021 09:43:46 +0100 Subject: [PATCH 068/101] Revert grouping endpoint routes to check e2e (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/main.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/jetstream/main.go b/src/jetstream/main.go index 3e77ae7061..6349a7a14e 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -1119,6 +1119,19 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, needSetupMiddleware bool) { // API endpoints with Swagger documentation and accessible with an API key that require admin permissions stableAdminAPIGroup := stableAPIGroup + if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled { + stableAdminAPIGroup.Use(p.endpointAdminMiddleware) + stableAdminAPIGroup.POST("/endpoints", p.pluginRegisterRouter) + stableAdminAPIGroup.POST("/endpoints/:id", p.updateEndpoint, p.endpointUpdateDeleteMiddleware) + stableAdminAPIGroup.DELETE("/endpoints/:id", p.unregisterCluster, p.endpointUpdateDeleteMiddleware) + } else { + stableAdminAPIGroup.Use(p.adminMiddleware) + stableAdminAPIGroup.POST("/endpoints", p.pluginRegisterRouter) + stableAdminAPIGroup.POST("/endpoints/:id", p.updateEndpoint) + stableAdminAPIGroup.DELETE("/endpoints/:id", p.unregisterCluster) + } + + /* comment this block out to test if e2e tests fail because of the new group stableEndpointAdminAPIGroup := stableAdminAPIGroup.Group("/endpoints") if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled { @@ -1145,6 +1158,7 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, needSetupMiddleware bool) { stableEndpointAdminAPIGroup.DELETE("/:id", p.unregisterCluster) } // sessionGroup.DELETE("/cnsis", p.removeCluster) + */ // Serve up static resources if staticDirErr == nil { From 2aa826537444fad41495253184b5ecb0a6890fb2 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Mon, 1 Mar 2021 15:15:47 +0100 Subject: [PATCH 069/101] Rename mock function (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/cnsi_test.go | 14 +++++++------- src/jetstream/mock_server_test.go | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/jetstream/cnsi_test.go b/src/jetstream/cnsi_test.go index 9d501f7468..f4331325e2 100644 --- a/src/jetstream/cnsi_test.go +++ b/src/jetstream/cnsi_test.go @@ -309,7 +309,7 @@ func TestRegisterWithUserEndpointsEnabled(t *testing.T) { Convey("as admin", func() { Convey("with overwrite disabled", func() { // setup - adminEndpoint := setupMockEndpointRequest(t, mockAdmin.ConnectedUser, mockV2Info[0], "CF Cluster 1", false) + adminEndpoint := setupMockEndpointRegisterRequest(t, mockAdmin.ConnectedUser, mockV2Info[0], "CF Cluster 1", false) if errSession := pp.setSessionValues(adminEndpoint.EchoContext, mockAdmin.SessionValues); errSession != nil { t.Error(errors.New("unable to mock/stub user in session object")) @@ -343,7 +343,7 @@ func TestRegisterWithUserEndpointsEnabled(t *testing.T) { }) Convey("overwrite existing user endpoints", func() { // setup - userEndpoint := setupMockEndpointRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1 User", false) + userEndpoint := setupMockEndpointRegisterRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1 User", false) // mock executions mockStratosAuth. @@ -375,7 +375,7 @@ func TestRegisterWithUserEndpointsEnabled(t *testing.T) { Convey("with overwrite enabled", func() { // setup - adminEndpoint := setupMockEndpointRequest(t, mockAdmin.ConnectedUser, mockV2Info[0], "CF Cluster 1", true) + adminEndpoint := setupMockEndpointRegisterRequest(t, mockAdmin.ConnectedUser, mockV2Info[0], "CF Cluster 1", true) if errSession := pp.setSessionValues(adminEndpoint.EchoContext, mockAdmin.SessionValues); errSession != nil { t.Error(errors.New("unable to mock/stub user in session object")) @@ -411,7 +411,7 @@ func TestRegisterWithUserEndpointsEnabled(t *testing.T) { Convey("overwrite existing user endpoints", func() { // setup - userEndpoint := setupMockEndpointRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1 User", false) + userEndpoint := setupMockEndpointRegisterRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1 User", false) // mock executions mockStratosAuth. @@ -451,7 +451,7 @@ func TestRegisterWithUserEndpointsEnabled(t *testing.T) { Convey("as user", func() { Convey("with overwrite disabled", func() { // setup - userEndpoint := setupMockEndpointRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1", false) + userEndpoint := setupMockEndpointRegisterRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1", false) if errSession := pp.setSessionValues(userEndpoint.EchoContext, mockUser1.SessionValues); errSession != nil { t.Error(errors.New("unable to mock/stub user in session object")) @@ -483,7 +483,7 @@ func TestRegisterWithUserEndpointsEnabled(t *testing.T) { }) }) Convey("register existing endpoint from different user", func() { - userEndpoint2 := setupMockEndpointRequest(t, mockUser2.ConnectedUser, mockV2Info[0], "CF Cluster 2", false) + userEndpoint2 := setupMockEndpointRegisterRequest(t, mockUser2.ConnectedUser, mockV2Info[0], "CF Cluster 2", false) // mock executions mockStratosAuth. @@ -536,7 +536,7 @@ func TestRegisterWithUserEndpointsEnabled(t *testing.T) { }) }) Convey("with overwrite enabled", func() { - userEndpoint := setupMockEndpointRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1", false) + userEndpoint := setupMockEndpointRegisterRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1", false) if errSession := pp.setSessionValues(userEndpoint.EchoContext, mockUser1.SessionValues); errSession != nil { t.Error(errors.New("unable to mock/stub user in session object")) diff --git a/src/jetstream/mock_server_test.go b/src/jetstream/mock_server_test.go index 4581bebf0c..53a4b2b027 100644 --- a/src/jetstream/mock_server_test.go +++ b/src/jetstream/mock_server_test.go @@ -272,7 +272,7 @@ func setupMockUser(guid string, admin bool, scopes []string) MockUser { } // mockV2Info needs to be closed -func setupMockEndpointRequest(t *testing.T, user *interfaces.ConnectedUser, mockV2Info *httptest.Server, endpointName string, overwriteEndpoints bool) MockEndpointRequest { +func setupMockEndpointRegisterRequest(t *testing.T, user *interfaces.ConnectedUser, mockV2Info *httptest.Server, endpointName string, overwriteEndpoints bool) MockEndpointRequest { // create a request for each endpoint req := setupMockReq("POST", "", map[string]string{ From 15cfc532a5f9a74316b415e8b78a14befbe8d4a8 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Mon, 1 Mar 2021 15:16:49 +0100 Subject: [PATCH 070/101] Rewrite error msg in middleware (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/middleware.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/jetstream/middleware.go b/src/jetstream/middleware.go index 5fd3303c56..846cb841fa 100644 --- a/src/jetstream/middleware.go +++ b/src/jetstream/middleware.go @@ -256,6 +256,7 @@ func (p *portalProxy) adminMiddleware(h echo.HandlerFunc) echo.HandlerFunc { func (p *portalProxy) endpointAdminMiddleware(h echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { log.Debug("endpointAdminMiddleware") + userID, err := p.GetSessionValue(c, "user_id") if err != nil { return c.NoContent(http.StatusUnauthorized) @@ -269,7 +270,7 @@ func (p *portalProxy) endpointAdminMiddleware(h echo.HandlerFunc) echo.HandlerFu endpointAdmin := strings.Contains(strings.Join(u.Scopes, ""), "stratos.endpointadmin") if endpointAdmin == false && u.Admin == false { - return handleSessionError(p.Config, c, errors.New("Unauthorized"), false, "You must be a Stratos admin or endpoint-admin to access this API") + return handleSessionError(p.Config, c, errors.New("Unauthorized"), false, "You must be a Stratos admin or endpointAdmin to access this API") } return h(c) @@ -305,7 +306,7 @@ func (p *portalProxy) endpointUpdateDeleteMiddleware(h echo.HandlerFunc) echo.Ha } if !adminEndpoint && !u.Admin && cnsiRecord.Creator != userID.(string) { - return handleSessionError(p.Config, c, errors.New("Unauthorized"), false, "Endpoint-admins are not allowed to modify endpoints created by other endpoint-admins.") + return handleSessionError(p.Config, c, errors.New("Unauthorized"), false, "EndpointAdmins are not allowed to modify endpoints created by other endpointAdmins.") } return h(c) From d223a6dc164b439d27537f8a14a6ce1e6d3a2bff Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Mon, 1 Mar 2021 15:17:26 +0100 Subject: [PATCH 071/101] Add unit tests for middleware (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/middleware_test.go | 240 +++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) diff --git a/src/jetstream/middleware_test.go b/src/jetstream/middleware_test.go index eb061d7b1d..326519cea4 100644 --- a/src/jetstream/middleware_test.go +++ b/src/jetstream/middleware_test.go @@ -3,6 +3,7 @@ package main import ( "database/sql" "errors" + "fmt" "net/http" "net/http/httptest" "testing" @@ -376,3 +377,242 @@ func Test_apiKeyMiddleware(t *testing.T) { }) }) } + +func TestEndpointAdminMiddleware(t *testing.T) { + t.Parallel() + + Convey("new request through endpointAdminMiddleware", t, func() { + // mock StratosAuthService + ctrl := gomock.NewController(t) + mockStratosAuth := mock_interfaces.NewMockStratosAuth(ctrl) + defer ctrl.Finish() + + // setup mock DB, PortalProxy and mock StratosAuthService + pp, db, _ := setupPortalProxyWithAuthService(mockStratosAuth) + defer db.Close() + + handlerFunc := func(c echo.Context) error { + return c.String(http.StatusOK, "test") + } + + middleware := pp.endpointAdminMiddleware(handlerFunc) + + mockAdmin := setupMockUser(mockAdminGUID, true, []string{}) + mockEndpointAdmin := setupMockUser(mockUserGUID+"1", false, []string{"stratos.endpointadmin"}) + mockUser := setupMockUser(mockUserGUID+"2", false, []string{}) + + Convey("as admin", func() { + ctx, _ := makeNewRequestWithParams("POST", map[string]string{}) + + if errSession := pp.setSessionValues(ctx, mockAdmin.SessionValues); errSession != nil { + t.Error(errors.New("unable to mock/stub user in session object")) + } + + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockAdmin.ConnectedUser.GUID)). + Return(mockAdmin.ConnectedUser, nil) + + err := middleware(ctx) + + Convey("request should be successful", func() { + So(err, ShouldBeNil) + }) + }) + + Convey("as endpointadmin", func() { + ctx, _ := makeNewRequestWithParams("POST", map[string]string{}) + + if errSession := pp.setSessionValues(ctx, mockEndpointAdmin.SessionValues); errSession != nil { + t.Error(errors.New("unable to mock/stub user in session object")) + } + + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockEndpointAdmin.ConnectedUser.GUID)). + Return(mockEndpointAdmin.ConnectedUser, nil) + + err := middleware(ctx) + + Convey("request should be successful", func() { + So(err, ShouldBeNil) + }) + + }) + Convey("as normal user", func() { + ctx, _ := makeNewRequestWithParams("POST", map[string]string{}) + + if errSession := pp.setSessionValues(ctx, mockUser.SessionValues); errSession != nil { + t.Error(errors.New("unable to mock/stub user in session object")) + } + + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockUser.ConnectedUser.GUID)). + Return(mockUser.ConnectedUser, nil) + + err := middleware(ctx) + + Convey("request should fail", func() { + So(err, + ShouldResemble, + handleSessionError(pp.Config, + ctx, + errors.New("Unauthorized"), + false, + "You must be a Stratos admin or endpointAdmin to access this API")) + }) + }) + }) +} + +func TestEndpointUpdateDeleteMiddleware(t *testing.T) { + t.Parallel() + + Convey("new request through endpointUpdateDeleteMiddleware", t, func() { + // mock StratosAuthService + ctrl := gomock.NewController(t) + mockStratosAuth := mock_interfaces.NewMockStratosAuth(ctrl) + defer ctrl.Finish() + + // setup mock DB, PortalProxy and mock StratosAuthService + pp, db, mock := setupPortalProxyWithAuthService(mockStratosAuth) + defer db.Close() + + res := httptest.NewRecorder() + req := setupMockReq("POST", "", nil) + _, ctx := setupEchoContext(res, req) + ctx.SetParamNames("id") + + handlerFunc := func(c echo.Context) error { + return c.String(http.StatusOK, "test") + } + middleware := pp.endpointUpdateDeleteMiddleware(handlerFunc) + + mockAdmin := setupMockUser(mockAdminGUID, true, []string{}) + mockEndpointAdmin1 := setupMockUser(mockUserGUID+"1", false, []string{"stratos.endpointadmin"}) + mockEndpointAdmin2 := setupMockUser(mockUserGUID+"2", false, []string{"stratos.endpointadmin"}) + + adminEndpointArgs := createEndpointRowArgs("CF Endpoint 1", "https://127.0.0.1:50001", mockAdmin.ConnectedUser.GUID, mockAdmin.ConnectedUser.Admin) + adminEndpointRows := sqlmock.NewRows(rowFieldsForCNSI).AddRow(adminEndpointArgs...) + + userEndpoint1Args := createEndpointRowArgs("CF Endpoint 2", "https://127.0.0.1:50002", mockEndpointAdmin1.ConnectedUser.GUID, mockEndpointAdmin1.ConnectedUser.Admin) + userEndpoint1Rows := sqlmock.NewRows(rowFieldsForCNSI).AddRow(userEndpoint1Args...) + + userEndpoint2Args := createEndpointRowArgs("CF Endpoint 3", "https://127.0.0.1:50003", mockEndpointAdmin2.ConnectedUser.GUID, mockEndpointAdmin2.ConnectedUser.Admin) + userEndpoint2Rows := sqlmock.NewRows(rowFieldsForCNSI).AddRow(userEndpoint2Args...) + + Convey("as admin", func() { + if errSession := pp.setSessionValues(ctx, mockAdmin.SessionValues); errSession != nil { + t.Error(errors.New("unable to mock/stub user in session object")) + } + Convey("edit admin endpoint", func() { + ctx.SetParamValues(fmt.Sprintf("%v", adminEndpointArgs[0])) + + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockAdmin.ConnectedUser.GUID)). + Return(mockAdmin.ConnectedUser, nil) + + mock.ExpectQuery(selectAnyFromCNSIs).WithArgs(adminEndpointArgs[0]).WillReturnRows(adminEndpointRows) + + err := middleware(ctx) + dberr := mock.ExpectationsWereMet() + + Convey("request should be successful", func() { + So(err, ShouldBeNil) + So(dberr, ShouldBeNil) + }) + }) + Convey("edit user endpoint", func() { + ctx.SetParamValues(fmt.Sprintf("%v", userEndpoint1Args[0])) + + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockAdmin.ConnectedUser.GUID)). + Return(mockAdmin.ConnectedUser, nil) + + mock.ExpectQuery(selectAnyFromCNSIs).WithArgs(userEndpoint1Args[0]).WillReturnRows(userEndpoint1Rows) + + err := middleware(ctx) + dberr := mock.ExpectationsWereMet() + + Convey("request should be successful", func() { + So(err, ShouldBeNil) + So(dberr, ShouldBeNil) + }) + }) + }) + Convey("as user", func() { + if errSession := pp.setSessionValues(ctx, mockEndpointAdmin1.SessionValues); errSession != nil { + t.Error(errors.New("unable to mock/stub user in session object")) + } + Convey("edit admin endpoint", func() { + ctx.SetParamValues(fmt.Sprintf("%v", adminEndpointArgs[0])) + + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockEndpointAdmin1.ConnectedUser.GUID)). + Return(mockEndpointAdmin1.ConnectedUser, nil) + + mock.ExpectQuery(selectAnyFromCNSIs).WithArgs(adminEndpointArgs[0]).WillReturnRows(adminEndpointRows) + + err := middleware(ctx) + dberr := mock.ExpectationsWereMet() + + Convey("request should fail", func() { + So(err, + ShouldResemble, + handleSessionError(pp.Config, + ctx, + errors.New("Unauthorized"), + false, + "You must be Stratos admin to modify this endpoint.")) + So(dberr, ShouldBeNil) + }) + }) + Convey("edit own endpoint", func() { + ctx.SetParamValues(fmt.Sprintf("%v", userEndpoint1Args[0])) + + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockEndpointAdmin1.ConnectedUser.GUID)). + Return(mockEndpointAdmin1.ConnectedUser, nil) + + mock.ExpectQuery(selectAnyFromCNSIs).WithArgs(userEndpoint1Args[0]).WillReturnRows(userEndpoint1Rows) + + err := middleware(ctx) + dberr := mock.ExpectationsWereMet() + + Convey("request should be successful", func() { + So(err, ShouldBeNil) + So(dberr, ShouldBeNil) + }) + }) + Convey("edit endpoint from different user", func() { + ctx.SetParamValues(fmt.Sprintf("%v", userEndpoint2Args[0])) + + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockEndpointAdmin1.ConnectedUser.GUID)). + Return(mockEndpointAdmin1.ConnectedUser, nil) + + mock.ExpectQuery(selectAnyFromCNSIs).WithArgs(userEndpoint2Args[0]).WillReturnRows(userEndpoint2Rows) + + err := middleware(ctx) + dberr := mock.ExpectationsWereMet() + + Convey("request should fail", func() { + So(err, + ShouldResemble, + handleSessionError(pp.Config, + ctx, + errors.New("Unauthorized"), + false, + "EndpointAdmins are not allowed to modify endpoints created by other endpointAdmins.")) + So(dberr, ShouldBeNil) + }) + }) + }) + }) +} From d49d99d9ad4e5e399da99d50ea1de0e4b205b3c3 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Tue, 2 Mar 2021 15:30:35 +0100 Subject: [PATCH 072/101] Add more arguments to createEndpointRowArgs (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/cnsi_test.go | 6 +++--- src/jetstream/middleware_test.go | 6 +++--- src/jetstream/mock_server_test.go | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/jetstream/cnsi_test.go b/src/jetstream/cnsi_test.go index f4331325e2..620923de0f 100644 --- a/src/jetstream/cnsi_test.go +++ b/src/jetstream/cnsi_test.go @@ -603,9 +603,9 @@ func TestListCNSIsWithUserEndpointsEnabled(t *testing.T) { mockUser1 := setupMockUser(mockUserGUID+"1", false, []string{"stratos.endpointadmin"}) mockUser2 := setupMockUser(mockUserGUID+"2", false, []string{"stratos.endpointadmin"}) - adminEndpointArgs := createEndpointRowArgs("CF Endpoint 1", "https://127.0.0.1:50001", mockAdmin.ConnectedUser.GUID, mockAdmin.ConnectedUser.Admin) - userEndpoint1Args := createEndpointRowArgs("CF Endpoint 2", "https://127.0.0.1:50002", mockUser1.ConnectedUser.GUID, mockUser1.ConnectedUser.Admin) - userEndpoint2Args := createEndpointRowArgs("CF Endpoint 3", "https://127.0.0.1:50003", mockUser2.ConnectedUser.GUID, mockUser2.ConnectedUser.Admin) + adminEndpointArgs := createEndpointRowArgs("CF Endpoint 1", "https://127.0.0.1:50001", mockAuthEndpoint, mockTokenEndpoint, mockAdmin.ConnectedUser.GUID, mockAdmin.ConnectedUser.Admin) + userEndpoint1Args := createEndpointRowArgs("CF Endpoint 2", "https://127.0.0.1:50002", mockAuthEndpoint, mockTokenEndpoint, mockUser1.ConnectedUser.GUID, mockUser1.ConnectedUser.Admin) + userEndpoint2Args := createEndpointRowArgs("CF Endpoint 3", "https://127.0.0.1:50003", mockAuthEndpoint, mockTokenEndpoint, mockUser2.ConnectedUser.GUID, mockUser2.ConnectedUser.Admin) adminRows := sqlmock.NewRows(rowFieldsForCNSI). AddRow(adminEndpointArgs...) diff --git a/src/jetstream/middleware_test.go b/src/jetstream/middleware_test.go index 326519cea4..70b5f3ebcd 100644 --- a/src/jetstream/middleware_test.go +++ b/src/jetstream/middleware_test.go @@ -493,13 +493,13 @@ func TestEndpointUpdateDeleteMiddleware(t *testing.T) { mockEndpointAdmin1 := setupMockUser(mockUserGUID+"1", false, []string{"stratos.endpointadmin"}) mockEndpointAdmin2 := setupMockUser(mockUserGUID+"2", false, []string{"stratos.endpointadmin"}) - adminEndpointArgs := createEndpointRowArgs("CF Endpoint 1", "https://127.0.0.1:50001", mockAdmin.ConnectedUser.GUID, mockAdmin.ConnectedUser.Admin) + adminEndpointArgs := createEndpointRowArgs("CF Endpoint 1", "https://127.0.0.1:50001", mockAuthEndpoint, mockTokenEndpoint, mockAdmin.ConnectedUser.GUID, mockAdmin.ConnectedUser.Admin) adminEndpointRows := sqlmock.NewRows(rowFieldsForCNSI).AddRow(adminEndpointArgs...) - userEndpoint1Args := createEndpointRowArgs("CF Endpoint 2", "https://127.0.0.1:50002", mockEndpointAdmin1.ConnectedUser.GUID, mockEndpointAdmin1.ConnectedUser.Admin) + userEndpoint1Args := createEndpointRowArgs("CF Endpoint 2", "https://127.0.0.1:50002", mockAuthEndpoint, mockTokenEndpoint, mockEndpointAdmin1.ConnectedUser.GUID, mockEndpointAdmin1.ConnectedUser.Admin) userEndpoint1Rows := sqlmock.NewRows(rowFieldsForCNSI).AddRow(userEndpoint1Args...) - userEndpoint2Args := createEndpointRowArgs("CF Endpoint 3", "https://127.0.0.1:50003", mockEndpointAdmin2.ConnectedUser.GUID, mockEndpointAdmin2.ConnectedUser.Admin) + userEndpoint2Args := createEndpointRowArgs("CF Endpoint 3", "https://127.0.0.1:50003", mockAuthEndpoint, mockTokenEndpoint, mockEndpointAdmin2.ConnectedUser.GUID, mockEndpointAdmin2.ConnectedUser.Admin) userEndpoint2Rows := sqlmock.NewRows(rowFieldsForCNSI).AddRow(userEndpoint2Args...) Convey("as admin", func() { diff --git a/src/jetstream/mock_server_test.go b/src/jetstream/mock_server_test.go index 53a4b2b027..58d1dfeec8 100644 --- a/src/jetstream/mock_server_test.go +++ b/src/jetstream/mock_server_test.go @@ -219,7 +219,7 @@ func expectEncryptedTokenRow(mockEncryptionKey []byte) sqlmock.Rows { AddRow(mockTokenGUID, encryptedUaaToken, encryptedUaaToken, mockTokenExpiry, false, "OAuth2", "", mockUserGUID, nil) } -func createEndpointRowArgs(endpointName string, APIEndpoint string, uaaUserGUID string, userAdmin bool) []driver.Value { +func createEndpointRowArgs(endpointName string, APIEndpoint string, authEndpoint string, tokenEndpoint string, uaaUserGUID string, userAdmin bool) []driver.Value { creatorGUID := "" h := sha1.New() @@ -230,7 +230,7 @@ func createEndpointRowArgs(endpointName string, APIEndpoint string, uaaUserGUID creatorGUID = uaaUserGUID } - return []driver.Value{base64.RawURLEncoding.EncodeToString(h.Sum(nil)), endpointName, "cf", APIEndpoint, mockAuthEndpoint, mockTokenEndpoint, mockDopplerEndpoint, true, mockClientId, cipherClientSecret, false, "", "", creatorGUID} + return []driver.Value{base64.RawURLEncoding.EncodeToString(h.Sum(nil)), endpointName, "cf", APIEndpoint, authEndpoint, tokenEndpoint, mockDopplerEndpoint, true, mockClientId, cipherClientSecret, false, "", "", creatorGUID} } func setupHTTPTest(req *http.Request) (*httptest.ResponseRecorder, *echo.Echo, echo.Context, *portalProxy, *sql.DB, sqlmock.Sqlmock) { From a3b7f503a5d971d26821084b4a1602b81fe8d3fc Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Tue, 2 Mar 2021 15:32:11 +0100 Subject: [PATCH 073/101] Add endpoint connect unit tests (4876) Signed-off-by: Thomas Quandt --- src/jetstream/auth_test.go | 202 +++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) diff --git a/src/jetstream/auth_test.go b/src/jetstream/auth_test.go index 5141eb0b51..5eeda668f1 100644 --- a/src/jetstream/auth_test.go +++ b/src/jetstream/auth_test.go @@ -2,12 +2,16 @@ package main import ( "errors" + "fmt" "net/http" + "net/http/httptest" "net/url" "strings" "testing" "time" + "github.com/golang/mock/gomock" + "github.com/labstack/echo/v4" uuid "github.com/satori/go.uuid" sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" @@ -16,6 +20,8 @@ import ( "github.com/cloudfoundry-incubator/stratos/src/jetstream/crypto" "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/smartystreets/goconvey/convey" ) @@ -570,7 +576,203 @@ func TestLoginToCNSIWithBadUserIDinSession(t *testing.T) { So(mock.ExpectationsWereMet(), ShouldBeNil) }) }) +} + +func TestLoginToCNSIWithUserEndpointsEnabled(t *testing.T) { + t.Parallel() + + Convey("Login to CNSI with UserEndpoints enabled", t, func() { + // mock StratosAuthService + ctrl := gomock.NewController(t) + mockStratosAuth := mock_interfaces.NewMockStratosAuth(ctrl) + defer ctrl.Finish() + + // setup mock DB, PortalProxy and mock StratosAuthService + pp, db, mock := setupPortalProxyWithAuthService(mockStratosAuth) + defer db.Close() + + pp.GetConfig().UserEndpointsEnabled = config.UserEndpointsConfigEnum.Enabled + + mockUAA := setupMockServer(t, + msRoute("/oauth/token"), + msMethod("POST"), + msStatus(http.StatusOK), + msBody(jsonMust(mockUAAResponse))) + defer mockUAA.Close() + + mockAdmin := setupMockUser(mockAdminGUID, true, []string{}) + mockEndpointAdmin1 := setupMockUser(mockUserGUID+"1", false, []string{"stratos.endpointadmin"}) + mockEndpointAdmin2 := setupMockUser(mockUserGUID+"2", false, []string{"stratos.endpointadmin"}) + + // setup everything to mock a connection to an admin endpoint + adminEndpointArgs := createEndpointRowArgs("CF Endpoint 1", mockUAA.URL, mockUAA.URL, mockUAA.URL, mockAdmin.ConnectedUser.GUID, mockAdmin.ConnectedUser.Admin) + adminEndpointRows := sqlmock.NewRows(rowFieldsForCNSI).AddRow(adminEndpointArgs...) + + res := httptest.NewRecorder() + req := setupMockReq("POST", "", map[string]string{ + "username": "admin", + "password": "changeme", + "cnsi_guid": fmt.Sprintf("%v", adminEndpointArgs[0]), + }) + _, ctxConnectToAdmin := setupEchoContext(res, req) + // setup everything to mock a connection to an user endpoint + userEndpoint1Args := createEndpointRowArgs("CF Endpoint 2", mockUAA.URL, mockUAA.URL, mockUAA.URL, mockEndpointAdmin1.ConnectedUser.GUID, mockEndpointAdmin1.ConnectedUser.Admin) + userEndpoint1Rows := sqlmock.NewRows(rowFieldsForCNSI).AddRow(userEndpoint1Args...) + + res = httptest.NewRecorder() + req = setupMockReq("POST", "", map[string]string{ + "username": "admin", + "password": "changeme", + "cnsi_guid": fmt.Sprintf("%v", userEndpoint1Args[0]), + }) + _, ctxConnectToUser1 := setupEchoContext(res, req) + + // setup everything to mock a connection to a different user endpoint + userEndpoint2Args := createEndpointRowArgs("CF Endpoint 3", mockUAA.URL, mockUAA.URL, mockUAA.URL, mockEndpointAdmin2.ConnectedUser.GUID, mockEndpointAdmin2.ConnectedUser.Admin) + userEndpoint2Rows := sqlmock.NewRows(rowFieldsForCNSI).AddRow(userEndpoint2Args...) + + res = httptest.NewRecorder() + req = setupMockReq("POST", "", map[string]string{ + "username": "admin", + "password": "changeme", + "cnsi_guid": fmt.Sprintf("%v", userEndpoint2Args[0]), + }) + _, ctxConnectToUser2 := setupEchoContext(res, req) + + Convey("As admin", func() { + + Convey("Connect to admin endpoint", func() { + if errSession := pp.setSessionValues(ctxConnectToAdmin, mockAdmin.SessionValues); errSession != nil { + t.Error(errors.New("unable to mock/stub user in session object")) + } + + mock.ExpectQuery(selectFromCNSIs).WillReturnRows(adminEndpointRows) + + mock.ExpectQuery(selectAnyFromTokens). + WithArgs(adminEndpointArgs[0], mockAdmin.ConnectedUser.GUID). + WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow("0")) + + mock.ExpectExec(insertIntoTokens). + WillReturnResult(sqlmock.NewResult(1, 1)) + + err := pp.loginToCNSI(ctxConnectToAdmin) + dberr := mock.ExpectationsWereMet() + + Convey("there should be no error", func() { + So(err, ShouldBeNil) + }) + + Convey("there should be no db error", func() { + So(dberr, ShouldBeNil) + }) + }) + Convey("Connect to user endpoint", func() { + if errSession := pp.setSessionValues(ctxConnectToUser1, mockAdmin.SessionValues); errSession != nil { + t.Error(errors.New("unable to mock/stub user in session object")) + } + + mock.ExpectQuery(selectFromCNSIs).WillReturnRows(userEndpoint1Rows) + + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockAdmin.ConnectedUser.GUID)). + Return(mockAdmin.ConnectedUser, nil) + + err := pp.loginToCNSI(ctxConnectToUser1) + dberr := mock.ExpectationsWereMet() + + Convey("should fail", func() { + So(err, ShouldResemble, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - admins are not allowed to connect to user created endpoints")) + }) + + Convey("there should be no db error", func() { + So(dberr, ShouldBeNil) + }) + + }) + }) + Convey("As user", func() { + Convey("Connect to own endpoint", func() { + if errSession := pp.setSessionValues(ctxConnectToUser1, mockEndpointAdmin1.SessionValues); errSession != nil { + t.Error(errors.New("unable to mock/stub user in session object")) + } + + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockEndpointAdmin1.ConnectedUser.GUID)). + Return(mockEndpointAdmin1.ConnectedUser, nil) + + mock.ExpectQuery(selectFromCNSIs).WillReturnRows(userEndpoint1Rows) + + mock.ExpectQuery(selectAnyFromTokens). + WithArgs(userEndpoint1Args[0], mockEndpointAdmin1.ConnectedUser.GUID). + WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow("0")) + + mock.ExpectExec(insertIntoTokens). + WillReturnResult(sqlmock.NewResult(1, 1)) + + err := pp.loginToCNSI(ctxConnectToUser1) + dberr := mock.ExpectationsWereMet() + + Convey("there should be no error", func() { + So(err, ShouldBeNil) + }) + + Convey("there should be no db error", func() { + So(dberr, ShouldBeNil) + }) + }) + Convey("Connect to admin endpoint", func() { + if errSession := pp.setSessionValues(ctxConnectToAdmin, mockEndpointAdmin1.SessionValues); errSession != nil { + t.Error(errors.New("unable to mock/stub user in session object")) + } + + mock.ExpectQuery(selectFromCNSIs).WillReturnRows(adminEndpointRows) + + mock.ExpectQuery(selectAnyFromTokens). + WithArgs(adminEndpointArgs[0], mockEndpointAdmin1.ConnectedUser.GUID). + WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow("0")) + + mock.ExpectExec(insertIntoTokens). + WillReturnResult(sqlmock.NewResult(1, 1)) + + err := pp.loginToCNSI(ctxConnectToAdmin) + dberr := mock.ExpectationsWereMet() + + Convey("there should be no error", func() { + So(err, ShouldBeNil) + }) + + Convey("there should be no db error", func() { + So(dberr, ShouldBeNil) + }) + }) + Convey("Connect to endpoint created by another user", func() { + if errSession := pp.setSessionValues(ctxConnectToUser2, mockEndpointAdmin1.SessionValues); errSession != nil { + t.Error(errors.New("unable to mock/stub user in session object")) + } + + mock.ExpectQuery(selectFromCNSIs).WillReturnRows(userEndpoint2Rows) + + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockEndpointAdmin1.ConnectedUser.GUID)). + Return(mockEndpointAdmin1.ConnectedUser, nil) + + err := pp.loginToCNSI(ctxConnectToUser2) + dberr := mock.ExpectationsWereMet() + + Convey("should fail", func() { + So(err, ShouldResemble, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - non-admins are not allowed to connect to endpoints created by other non-admins")) + }) + + Convey("there should be no db error", func() { + So(dberr, ShouldBeNil) + }) + }) + }) + }) } func TestLogout(t *testing.T) { From ac6ab4a34f9c2d51b4b8782a033c44242f342714 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Tue, 2 Mar 2021 17:13:14 +0100 Subject: [PATCH 074/101] Change created by to creator (#4876) Signed-off-by: Thomas Quandt --- .../endpoint/endpoint-card/endpoint-card.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html index 9497b4da2a..d00af9b383 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html @@ -45,7 +45,7 @@ - Created by + Creator
{{ row.creator.name }}
From e28003f3604f5d57c736a64dac22e4f8d1bc8cc8 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Tue, 2 Mar 2021 17:14:32 +0100 Subject: [PATCH 075/101] Add creator to endpoint table UI (#4876) Signed-off-by: Thomas Quandt --- .../endpoint/endpoints-list-config.service.ts | 31 +++++++++++++--- ...table-cell-endpoint-creator.component.html | 3 ++ ...table-cell-endpoint-creator.component.scss | 0 ...le-cell-endpoint-creator.component.spec.ts | 35 +++++++++++++++++++ .../table-cell-endpoint-creator.component.ts | 30 ++++++++++++++++ .../packages/core/src/shared/shared.module.ts | 2 ++ 6 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.html create mode 100644 src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.scss create mode 100644 src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.spec.ts create mode 100644 src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.ts diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts index 24fb90718b..7a2e2c7d2e 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts @@ -1,6 +1,6 @@ -import { Injectable } from '@angular/core'; +import { Injectable, OnDestroy } from '@angular/core'; import { Store } from '@ngrx/store'; -import { BehaviorSubject, combineLatest, of } from 'rxjs'; +import { BehaviorSubject, combineLatest, of, Subscription } from 'rxjs'; import { debounceTime, filter, map } from 'rxjs/operators'; import { ListView } from '../../../../../../../store/src/actions/list.actions'; @@ -14,6 +14,7 @@ import { stratosEntityCatalog } from '../../../../../../../store/src/stratos-ent import { EndpointModel } from '../../../../../../../store/src/types/endpoint.types'; import { PaginationEntityState } from '../../../../../../../store/src/types/pagination.types'; import { UserFavoriteManager } from '../../../../../../../store/src/user-favorite-manager'; +import { SessionService } from '../../../../services/session.service'; import { createTableColumnFavorite } from '../../list-table/table-cell-favorite/table-cell-favorite.component'; import { ITableColumn } from '../../list-table/table.types'; import { @@ -28,6 +29,7 @@ import { EndpointCardComponent } from './endpoint-card/endpoint-card.component'; import { EndpointListHelper } from './endpoint-list.helpers'; import { EndpointsDataSource } from './endpoints-data-source'; import { TableCellEndpointAddressComponent } from './table-cell-endpoint-address/table-cell-endpoint-address.component'; +import { TableCellEndpointCreatorComponent } from './table-cell-endpoint-creator/table-cell-endpoint-creator.component'; import { TableCellEndpointDetailsComponent } from './table-cell-endpoint-details/table-cell-endpoint-details.component'; import { TableCellEndpointNameComponent } from './table-cell-endpoint-name/table-cell-endpoint-name.component'; import { TableCellEndpointStatusComponent } from './table-cell-endpoint-status/table-cell-endpoint-status.component'; @@ -35,7 +37,7 @@ import { TableCellEndpointStatusComponent } from './table-cell-endpoint-status/t @Injectable() -export class EndpointsListConfigService implements IListConfig { +export class EndpointsListConfigService implements IListConfig, OnDestroy { cardComponent = EndpointCardComponent; private singleActions: IListAction[]; @@ -107,6 +109,7 @@ export class EndpointsListConfigService implements IListConfig { filter: 'Filter Endpoints' }; enableTextFilter = true; + userEndpointsSubscription: Subscription; constructor( private store: Store, @@ -114,13 +117,29 @@ export class EndpointsListConfigService implements IListConfig { entityMonitorFactory: EntityMonitorFactory, internalEventMonitorFactory: InternalEventMonitorFactory, endpointListHelper: EndpointListHelper, - userFavoriteManager: UserFavoriteManager + userFavoriteManager: UserFavoriteManager, + sessionService: SessionService ) { this.singleActions = endpointListHelper.endpointActions(); const favoriteCell = createTableColumnFavorite( (row: EndpointModel) => userFavoriteManager.getFavoriteEndpointFromEntity(row) ); this.columns.push(favoriteCell); + this.userEndpointsSubscription = sessionService.userEndpointsNotDisabled().subscribe(enabled => { + if (enabled){ + this.columns.splice(4, 0, { + columnId: 'creator', + headerCell: () => 'Creator', + cellComponent: TableCellEndpointCreatorComponent, + sort: { + type: 'sort', + orderKey: 'creator', + field: 'creator.name' + }, + cellFlex: '2' + }); + } + }); this.dataSource = new EndpointsDataSource( this.store, @@ -201,4 +220,8 @@ export class EndpointsListConfigService implements IListConfig { ); } } + + ngOnDestroy() { + this.userEndpointsSubscription.unsubscribe(); + } } diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.html b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.html new file mode 100644 index 0000000000..550579e875 --- /dev/null +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.html @@ -0,0 +1,3 @@ +
+ {{ creator }} +
\ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.scss b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.spec.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.spec.ts new file mode 100644 index 0000000000..df3ba7cf20 --- /dev/null +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.spec.ts @@ -0,0 +1,35 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CoreModule } from '@stratosui/core'; +import { EndpointModel } from '@stratosui/store'; + +import { TableCellEndpointCreatorComponent } from './table-cell-endpoint-creator.component'; + +describe('TableCellEndpointCreatorComponent', () => { + let component: TableCellEndpointCreatorComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ TableCellEndpointCreatorComponent ], + imports: [ + CoreModule + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TableCellEndpointCreatorComponent); + component = fixture.componentInstance; + component.row = { + creator: { + name: 'dummy' + } + } as EndpointModel; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.ts new file mode 100644 index 0000000000..16e53eb05c --- /dev/null +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.ts @@ -0,0 +1,30 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { EndpointModel, entityCatalog } from '@stratosui/store'; +import { TableCellCustom } from '../../../list.types'; + +@Component({ + selector: 'app-table-cell-endpoint-creator', + templateUrl: './table-cell-endpoint-creator.component.html', + styleUrls: ['./table-cell-endpoint-creator.component.scss'] +}) +export class TableCellEndpointCreatorComponent extends TableCellCustom implements OnInit { + + public creator = ''; + + @Input() + get row(): EndpointModel { + return super.row; + } + set row(row: EndpointModel) { + super.row = row; + } + + constructor() { + super(); + } + + ngOnInit(): void { + this.creator = this.row.creator.name; + } + +} diff --git a/src/frontend/packages/core/src/shared/shared.module.ts b/src/frontend/packages/core/src/shared/shared.module.ts index 38a45ca14b..b6c54ff774 100644 --- a/src/frontend/packages/core/src/shared/shared.module.ts +++ b/src/frontend/packages/core/src/shared/shared.module.ts @@ -119,6 +119,7 @@ import { LongRunningOperationsService } from './services/long-running-op.service import { MetricsRangeSelectorService } from './services/metrics-range-selector.service'; import { SessionService } from './services/session.service'; import { UserPermissionDirective } from './user-permission.directive'; +import { TableCellEndpointCreatorComponent } from './components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component'; @NgModule({ @@ -221,6 +222,7 @@ import { UserPermissionDirective } from './user-permission.directive'; CardProgressOverlayComponent, MaxListMessageComponent, ProfileSettingsComponent, + TableCellEndpointCreatorComponent, ], exports: [ ApplicationStateIconPipe, From ac80cfa447f8034968afd7385f37d567124be96e Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Wed, 3 Mar 2021 10:30:49 +0100 Subject: [PATCH 076/101] Add website documentation (#4876) Signed-off-by: Thomas Quandt --- website/docs/endpoints/cf/user-endpoints.md | 44 +++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 website/docs/endpoints/cf/user-endpoints.md diff --git a/website/docs/endpoints/cf/user-endpoints.md b/website/docs/endpoints/cf/user-endpoints.md new file mode 100644 index 0000000000..aea30ee546 --- /dev/null +++ b/website/docs/endpoints/cf/user-endpoints.md @@ -0,0 +1,44 @@ +--- +title: Configuring User Endpoints +sidebar_label: Configuring User Endpoints +--- + +Stratos provides a way for users to create endpoints without the need to be an administrator. + +> Note: Admin endpoint-ID's are generated through a SHA-1 encryption of the URL. Endpoints created by a user will differ in their ID, by using the URL + user-ID for encryption. This should pose no problem in the usual Stratos workflow, but if you depend on the ID to be based solely on the URL, then use this feature with caution. + +## Set up + +In order to enable User Endpoints support in Stratos: + +1. The environment variable `USER_ENDPOINTS_ENABLED` must be set +2. The UAA client used by Stratos needs an additional scope `stratos.endpointadmin` +3. Users need to have the `stratos.endpointadmin` group attached to them + +Once all steps have been completed, user within the `stratos.endpointadmin` group are allowed to create endpoints. Endpoints created by users are only visible to their respective user and all admins. + +## Environment variable + +`USER_ENDPOINTS_ENABLED` can be set to three different states: + +1. `disabled` (default) will disable this feature. Neither admins nor users will see user endpoints. +2. `admin_only` will hide user endpoints from users. Admins can still see all endpoints created by users. +3. `enabled` will allow users within the `stratos.endpointadmin` group to create endpoints. The endpoints will only be visible to them or admins. + + +## Adding scopes to the UAA client + +To add the scope to a client, modify the following [UAA CLI](https://github.com/cloudfoundry/cf-uaac) command: + +``` +uaac client update CLIENT_NAME --scope "OTHER_SCOPES stratos.endpointadmin" +``` + +Replace `CLIENT_NAME` with the used client and `OTHER_SCOPES` with the current configured scopes. + +To add the group and add users to it, use: + +``` +uaac group add stratos.endpointadmin +uaac member add stratos.endpointadmin USER_NAME +``` From b1ab2e78efc343619b7ee04128f0c0b501cb2e4a Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Wed, 3 Mar 2021 11:39:11 +0100 Subject: [PATCH 077/101] Update desktop endpoint list functions (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/plugins/desktop/endpoints.go | 10 ++++++++-- src/jetstream/plugins/desktop/kubernetes/endpoints.go | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/jetstream/plugins/desktop/endpoints.go b/src/jetstream/plugins/desktop/endpoints.go index 4de98370f1..b7feb96efa 100644 --- a/src/jetstream/plugins/desktop/endpoints.go +++ b/src/jetstream/plugins/desktop/endpoints.go @@ -42,11 +42,17 @@ func (d *DesktopEndpointStore) FindByAPIEndpoint(endpoint string, encryptionKey } func (d *DesktopEndpointStore) ListByAPIEndpoint(endpoint string, encryptionKey []byte) ([]*interfaces.CNSIRecord, error) { - return d.store.ListByAPIEndpoint(endpoint, encryptionKey) + local, err := ListCloudFoundry() + db, err := d.store.ListByAPIEndpoint(endpoint, encryptionKey) + merged := mergeEndpoints(db, local) + return merged, err } func (d *DesktopEndpointStore) ListByCreator(userGUID string, encryptionKey []byte) ([]*interfaces.CNSIRecord, error) { - return d.store.ListByCreator(userGUID, encryptionKey) + local, err := ListCloudFoundry() + db, err := d.store.ListByCreator(userGUID, encryptionKey) + merged := mergeEndpoints(db, local) + return merged, err } func (d *DesktopEndpointStore) Delete(guid string) error { diff --git a/src/jetstream/plugins/desktop/kubernetes/endpoints.go b/src/jetstream/plugins/desktop/kubernetes/endpoints.go index 5f01a52cfc..3994c77713 100644 --- a/src/jetstream/plugins/desktop/kubernetes/endpoints.go +++ b/src/jetstream/plugins/desktop/kubernetes/endpoints.go @@ -47,11 +47,17 @@ func (d *EndpointStore) FindByAPIEndpoint(endpoint string, encryptionKey []byte) } func (d *EndpointStore) ListByAPIEndpoint(endpoint string, encryptionKey []byte) ([]*interfaces.CNSIRecord, error) { - return d.store.ListByAPIEndpoint(endpoint, encryptionKey) + local, _, err := ListKubernetes() + db, err := d.store.ListByAPIEndpoint(endpoint, encryptionKey) + merged := mergeEndpoints(db, local) + return merged, err } func (d *EndpointStore) ListByCreator(userGUID string, encryptionKey []byte) ([]*interfaces.CNSIRecord, error) { - return d.store.ListByCreator(userGUID, encryptionKey) + local, _, err := ListKubernetes() + db, err := d.store.ListByCreator(userGUID, encryptionKey) + merged := mergeEndpoints(db, local) + return merged, err } func (d *EndpointStore) Delete(guid string) error { From 82896acfa79a478c57b24ddcd943af12f22e547f Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Wed, 3 Mar 2021 12:12:26 +0100 Subject: [PATCH 078/101] Add dedicated endpoint group to routes (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/main.go | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/jetstream/main.go b/src/jetstream/main.go index 6349a7a14e..1f9a17843d 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -1119,9 +1119,13 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, needSetupMiddleware bool) { // API endpoints with Swagger documentation and accessible with an API key that require admin permissions stableAdminAPIGroup := stableAPIGroup + /* delete after tests pass if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled { stableAdminAPIGroup.Use(p.endpointAdminMiddleware) stableAdminAPIGroup.POST("/endpoints", p.pluginRegisterRouter) + // Use middleware in route directly, because documentation is faulty + // Apply middleware to group with .Use() when this issue is resolved: + // https://github.com/labstack/echo/issues/1519 stableAdminAPIGroup.POST("/endpoints/:id", p.updateEndpoint, p.endpointUpdateDeleteMiddleware) stableAdminAPIGroup.DELETE("/endpoints/:id", p.unregisterCluster, p.endpointUpdateDeleteMiddleware) } else { @@ -1130,35 +1134,26 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, needSetupMiddleware bool) { stableAdminAPIGroup.POST("/endpoints/:id", p.updateEndpoint) stableAdminAPIGroup.DELETE("/endpoints/:id", p.unregisterCluster) } + */ - /* comment this block out to test if e2e tests fail because of the new group stableEndpointAdminAPIGroup := stableAdminAPIGroup.Group("/endpoints") if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled { stableEndpointAdminAPIGroup.Use(p.endpointAdminMiddleware) - } else { - stableEndpointAdminAPIGroup.Use(p.adminMiddleware) - } - - // route endpoint creation requests to respecive plugins - stableEndpointAdminAPIGroup.POST("", p.pluginRegisterRouter) - - // Use middleware in route directly, because documentation is faulty - // Apply middleware to group with .Use() when this issue is resolved: - // https://github.com/labstack/echo/issues/1519 - - // do additional checks if user endpoints are enabled - if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled { - // Apply edits for the given endpoint + stableEndpointAdminAPIGroup.POST("", p.pluginRegisterRouter) + // Use middleware in route directly, because documentation is faulty + // Apply middleware to group with .Use() when this issue is resolved: + // https://github.com/labstack/echo/issues/1519 stableEndpointAdminAPIGroup.POST("/:id", p.updateEndpoint, p.endpointUpdateDeleteMiddleware) stableEndpointAdminAPIGroup.DELETE("/:id", p.unregisterCluster, p.endpointUpdateDeleteMiddleware) } else { - // Apply edits for the given endpoint + stableEndpointAdminAPIGroup.Use(p.adminMiddleware) + stableEndpointAdminAPIGroup.POST("", p.pluginRegisterRouter) stableEndpointAdminAPIGroup.POST("/:id", p.updateEndpoint) stableEndpointAdminAPIGroup.DELETE("/:id", p.unregisterCluster) } + // sessionGroup.DELETE("/cnsis", p.removeCluster) - */ // Serve up static resources if staticDirErr == nil { From b5bccff87c0d90d8d9aa903bd9bc74c3dbe18f09 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Wed, 3 Mar 2021 14:35:52 +0100 Subject: [PATCH 079/101] Fix group for endpoints (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/main.go | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/src/jetstream/main.go b/src/jetstream/main.go index 1f9a17843d..4bbef050a3 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -1119,38 +1119,23 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, needSetupMiddleware bool) { // API endpoints with Swagger documentation and accessible with an API key that require admin permissions stableAdminAPIGroup := stableAPIGroup - /* delete after tests pass - if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled { - stableAdminAPIGroup.Use(p.endpointAdminMiddleware) - stableAdminAPIGroup.POST("/endpoints", p.pluginRegisterRouter) - // Use middleware in route directly, because documentation is faulty - // Apply middleware to group with .Use() when this issue is resolved: - // https://github.com/labstack/echo/issues/1519 - stableAdminAPIGroup.POST("/endpoints/:id", p.updateEndpoint, p.endpointUpdateDeleteMiddleware) - stableAdminAPIGroup.DELETE("/endpoints/:id", p.unregisterCluster, p.endpointUpdateDeleteMiddleware) - } else { - stableAdminAPIGroup.Use(p.adminMiddleware) - stableAdminAPIGroup.POST("/endpoints", p.pluginRegisterRouter) - stableAdminAPIGroup.POST("/endpoints/:id", p.updateEndpoint) - stableAdminAPIGroup.DELETE("/endpoints/:id", p.unregisterCluster) - } - */ - - stableEndpointAdminAPIGroup := stableAdminAPIGroup.Group("/endpoints") + // If path "/endpoints" is used, then stableAPIGroup.GET("/endpoints", p.listCNSIs) won't be executed anymore + // static html will be returned instead. That's why we use the path "" + stableEndpointAdminAPIGroup := stableAdminAPIGroup.Group("") if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled { stableEndpointAdminAPIGroup.Use(p.endpointAdminMiddleware) - stableEndpointAdminAPIGroup.POST("", p.pluginRegisterRouter) + stableEndpointAdminAPIGroup.POST("/endpoints", p.pluginRegisterRouter) // Use middleware in route directly, because documentation is faulty // Apply middleware to group with .Use() when this issue is resolved: // https://github.com/labstack/echo/issues/1519 - stableEndpointAdminAPIGroup.POST("/:id", p.updateEndpoint, p.endpointUpdateDeleteMiddleware) - stableEndpointAdminAPIGroup.DELETE("/:id", p.unregisterCluster, p.endpointUpdateDeleteMiddleware) + stableEndpointAdminAPIGroup.POST("/endpoints:id", p.updateEndpoint, p.endpointUpdateDeleteMiddleware) + stableEndpointAdminAPIGroup.DELETE("/endpoints/:id", p.unregisterCluster, p.endpointUpdateDeleteMiddleware) } else { stableEndpointAdminAPIGroup.Use(p.adminMiddleware) - stableEndpointAdminAPIGroup.POST("", p.pluginRegisterRouter) - stableEndpointAdminAPIGroup.POST("/:id", p.updateEndpoint) - stableEndpointAdminAPIGroup.DELETE("/:id", p.unregisterCluster) + stableEndpointAdminAPIGroup.POST("/endpoints", p.pluginRegisterRouter) + stableEndpointAdminAPIGroup.POST("/endpoints/:id", p.updateEndpoint) + stableEndpointAdminAPIGroup.DELETE("/endpoints/:id", p.unregisterCluster) } // sessionGroup.DELETE("/cnsis", p.removeCluster) From 995759c3562c74963d9d52bfc8fdd65dbfd31d50 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Wed, 3 Mar 2021 14:37:35 +0100 Subject: [PATCH 080/101] Remove space character (#4876) Signed-off-by: Thomas Quandt --- .../list/list-types/endpoint/endpoints-list-config.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts index 7a2e2c7d2e..4c165becef 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts @@ -127,7 +127,7 @@ export class EndpointsListConfigService implements IListConfig, O this.columns.push(favoriteCell); this.userEndpointsSubscription = sessionService.userEndpointsNotDisabled().subscribe(enabled => { if (enabled){ - this.columns.splice(4, 0, { + this.columns.splice(4, 0, { columnId: 'creator', headerCell: () => 'Creator', cellComponent: TableCellEndpointCreatorComponent, From 89ba6655debbbb73cdee4df789688786a9cf2dad Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Wed, 3 Mar 2021 16:05:27 +0100 Subject: [PATCH 081/101] Add pgsql unit tests (#4876) Signed-off-by: Thomas Quandt --- .../repository/cnsis/pgsql_cnsis_test.go | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) diff --git a/src/jetstream/repository/cnsis/pgsql_cnsis_test.go b/src/jetstream/repository/cnsis/pgsql_cnsis_test.go index 3ae432aca8..68ed59ba86 100644 --- a/src/jetstream/repository/cnsis/pgsql_cnsis_test.go +++ b/src/jetstream/repository/cnsis/pgsql_cnsis_test.go @@ -305,6 +305,255 @@ func TestPgSQLCNSIs(t *testing.T) { }) }) + Convey("Given a request for a list of CNSIs from a creator", t, func() { + + var ( + rowFieldsForCluster = []string{"guid", "name", "cnsi_type", "api_endpoint", "doppler_logging_endpoint", "account", "token_expiry", "skip_ssl_validation", "disconnected", "meta_data", "sub_type", "endpoint_metadata", "creator"} + expectedList []*interfaces.CNSIRecord + mockAccount = "asd-gjfg-bob" + ) + + // general setup + expectedList = make([]*interfaces.CNSIRecord, 0) + + db, mock, err := sqlmock.New() + if err != nil { + t.Errorf("an error '%s' was not expected when opening a stub database connection", err) + } + defer db.Close() + Convey("if no records exist in the database", func() { + + rs := sqlmock.NewRows(rowFieldsForCluster) + mock.ExpectQuery(selectFromCNSIsWhere). + WillReturnRows(rs) + + // Expectations + Convey("No CNSIs should be returned", func() { + repository, _ := NewPostgresCNSIRepository(db) + results, _ := repository.ListByCreator(mockAccount, mockEncryptionKey) + So(len(results), ShouldEqual, 0) + + dberr := mock.ExpectationsWereMet() + So(dberr, ShouldBeNil) + }) + + Convey("the list of returned CNSIs should be empty", func() { + repository, _ := NewPostgresCNSIRepository(db) + results, _ := repository.ListByCreator(mockAccount, mockEncryptionKey) + So(results, ShouldResemble, expectedList) + + dberr := mock.ExpectationsWereMet() + So(dberr, ShouldBeNil) + }) + + Convey("there should be no error returned", func() { + repository, _ := NewPostgresCNSIRepository(db) + _, err := repository.ListByCreator(mockAccount, mockEncryptionKey) + So(err, ShouldBeNil) + + dberr := mock.ExpectationsWereMet() + So(dberr, ShouldBeNil) + }) + }) + + Convey("if 2 records exist in the database", func() { + + var ( + mockClusterList sqlmock.Rows + ) + + // general setup + u, _ := url.Parse(mockAPIEndpoint) + r1 := &interfaces.CNSIRecord{GUID: mockCFGUID, Name: "Some fancy CF Cluster", CNSIType: "cf", APIEndpoint: u, AuthorizationEndpoint: mockAuthEndpoint, TokenEndpoint: mockAuthEndpoint, DopplerLoggingEndpoint: mockDopplerEndpoint, SkipSSLValidation: true, ClientId: mockClientId, ClientSecret: mockClientSecret, SSOAllowed: false, Creator: mockAccount} + r2 := &interfaces.CNSIRecord{GUID: mockCEGUID, Name: "Some fancy HCE Cluster", CNSIType: "hce", APIEndpoint: u, AuthorizationEndpoint: mockAuthEndpoint, TokenEndpoint: mockAuthEndpoint, DopplerLoggingEndpoint: "", SkipSSLValidation: true, ClientId: mockClientId, ClientSecret: mockClientSecret, SSOAllowed: false, Creator: mockAccount} + expectedList = append(expectedList, r1, r2) + + mockClusterList = sqlmock.NewRows(rowFieldsForCNSI). + AddRow(mockCFGUID, "Some fancy CF Cluster", "cf", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, mockDopplerEndpoint, true, mockClientId, cipherClientSecret, false, "", "", mockAccount). + AddRow(mockCEGUID, "Some fancy HCE Cluster", "hce", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, "", true, mockClientId, cipherClientSecret, false, "", "", mockAccount) + mock.ExpectQuery(selectFromCNSIsWhere). + WillReturnRows(mockClusterList) + + // Expectations + Convey("2 CNSIs should be returned", func() { + repository, _ := NewPostgresCNSIRepository(db) + results, _ := repository.ListByCreator(mockAccount, mockEncryptionKey) + So(len(results), ShouldEqual, 2) + + dberr := mock.ExpectationsWereMet() + So(dberr, ShouldBeNil) + }) + + Convey("the list returned should match the expected list", func() { + repository, _ := NewPostgresCNSIRepository(db) + results, _ := repository.ListByCreator(mockAccount, mockEncryptionKey) + So(results, ShouldResemble, expectedList) + + dberr := mock.ExpectationsWereMet() + So(dberr, ShouldBeNil) + }) + + Convey("there should be no error returned", func() { + repository, _ := NewPostgresCNSIRepository(db) + _, err := repository.ListByCreator(mockAccount, mockEncryptionKey) + So(err, ShouldBeNil) + + dberr := mock.ExpectationsWereMet() + So(dberr, ShouldBeNil) + }) + }) + + Convey("If the database call fails for some reason", func() { + + expectedErrorMessage := fmt.Sprintf("Unable to retrieve CNSI records: %s", unknownDBError) + + mock.ExpectQuery(selectAnyFromCNSIs). + WillReturnError(errors.New(unknownDBError)) + + Convey("the returned value should be nil", func() { + repository, _ := NewPostgresCNSIRepository(db) + results, _ := repository.ListByCreator(mockAccount, mockEncryptionKey) + So(results, ShouldBeNil) + + dberr := mock.ExpectationsWereMet() + So(dberr, ShouldBeNil) + }) + + Convey("there should be a 'not found' error returned", func() { + repository, _ := NewPostgresCNSIRepository(db) + _, err := repository.ListByCreator(mockAccount, mockEncryptionKey) + So(err, ShouldResemble, errors.New(expectedErrorMessage)) + + dberr := mock.ExpectationsWereMet() + So(dberr, ShouldBeNil) + }) + }) + }) + + Convey("Given a request for a list of CNSIs with a given APIEndpoint string", t, func() { + + var ( + rowFieldsForCluster = []string{"guid", "name", "cnsi_type", "api_endpoint", "doppler_logging_endpoint", "account", "token_expiry", "skip_ssl_validation", "disconnected", "meta_data", "sub_type", "endpoint_metadata", "creator"} + expectedList []*interfaces.CNSIRecord + ) + + // general setup + expectedList = make([]*interfaces.CNSIRecord, 0) + + db, mock, err := sqlmock.New() + if err != nil { + t.Errorf("an error '%s' was not expected when opening a stub database connection", err) + } + defer db.Close() + Convey("if no records exist in the database", func() { + + rs := sqlmock.NewRows(rowFieldsForCluster) + mock.ExpectQuery(selectFromCNSIsWhere). + WillReturnRows(rs) + + // Expectations + Convey("No CNSIs should be returned", func() { + repository, _ := NewPostgresCNSIRepository(db) + results, _ := repository.ListByAPIEndpoint(mockAPIEndpoint, mockEncryptionKey) + So(len(results), ShouldEqual, 0) + + dberr := mock.ExpectationsWereMet() + So(dberr, ShouldBeNil) + }) + + Convey("the list of returned CNSIs should be empty", func() { + repository, _ := NewPostgresCNSIRepository(db) + results, _ := repository.ListByAPIEndpoint(mockAPIEndpoint, mockEncryptionKey) + So(results, ShouldResemble, expectedList) + + dberr := mock.ExpectationsWereMet() + So(dberr, ShouldBeNil) + }) + + Convey("there should be no error returned", func() { + repository, _ := NewPostgresCNSIRepository(db) + _, err := repository.ListByAPIEndpoint(mockAPIEndpoint, mockEncryptionKey) + So(err, ShouldBeNil) + + dberr := mock.ExpectationsWereMet() + So(dberr, ShouldBeNil) + }) + }) + + Convey("if 2 records exist in the database", func() { + + var ( + mockClusterList sqlmock.Rows + ) + + // general setup + u, _ := url.Parse(mockAPIEndpoint) + r1 := &interfaces.CNSIRecord{GUID: mockCFGUID, Name: "Some fancy CF Cluster", CNSIType: "cf", APIEndpoint: u, AuthorizationEndpoint: mockAuthEndpoint, TokenEndpoint: mockAuthEndpoint, DopplerLoggingEndpoint: mockDopplerEndpoint, SkipSSLValidation: true, ClientId: mockClientId, ClientSecret: mockClientSecret, SSOAllowed: false, Creator: ""} + r2 := &interfaces.CNSIRecord{GUID: mockCEGUID, Name: "Some fancy HCE Cluster", CNSIType: "hce", APIEndpoint: u, AuthorizationEndpoint: mockAuthEndpoint, TokenEndpoint: mockAuthEndpoint, DopplerLoggingEndpoint: "", SkipSSLValidation: true, ClientId: mockClientId, ClientSecret: mockClientSecret, SSOAllowed: false, Creator: ""} + expectedList = append(expectedList, r1, r2) + + mockClusterList = sqlmock.NewRows(rowFieldsForCNSI). + AddRow(mockCFGUID, "Some fancy CF Cluster", "cf", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, mockDopplerEndpoint, true, mockClientId, cipherClientSecret, false, "", "", ""). + AddRow(mockCEGUID, "Some fancy HCE Cluster", "hce", mockAPIEndpoint, mockAuthEndpoint, mockAuthEndpoint, "", true, mockClientId, cipherClientSecret, false, "", "", "") + mock.ExpectQuery(selectFromCNSIsWhere). + WillReturnRows(mockClusterList) + + // Expectations + Convey("2 CNSIs should be returned", func() { + repository, _ := NewPostgresCNSIRepository(db) + results, _ := repository.ListByAPIEndpoint(mockAPIEndpoint, mockEncryptionKey) + So(len(results), ShouldEqual, 2) + + dberr := mock.ExpectationsWereMet() + So(dberr, ShouldBeNil) + }) + + Convey("the list returned should match the expected list", func() { + repository, _ := NewPostgresCNSIRepository(db) + results, _ := repository.ListByAPIEndpoint(mockAPIEndpoint, mockEncryptionKey) + So(results, ShouldResemble, expectedList) + + dberr := mock.ExpectationsWereMet() + So(dberr, ShouldBeNil) + }) + + Convey("there should be no error returned", func() { + repository, _ := NewPostgresCNSIRepository(db) + _, err := repository.ListByAPIEndpoint(mockAPIEndpoint, mockEncryptionKey) + So(err, ShouldBeNil) + + dberr := mock.ExpectationsWereMet() + So(dberr, ShouldBeNil) + }) + }) + + Convey("If the database call fails for some reason", func() { + + expectedErrorMessage := fmt.Sprintf("Unable to retrieve CNSI records: %s", unknownDBError) + + mock.ExpectQuery(selectAnyFromCNSIs). + WillReturnError(errors.New(unknownDBError)) + + Convey("the returned value should be nil", func() { + repository, _ := NewPostgresCNSIRepository(db) + results, _ := repository.ListByAPIEndpoint(mockAPIEndpoint, mockEncryptionKey) + So(results, ShouldBeNil) + + dberr := mock.ExpectationsWereMet() + So(dberr, ShouldBeNil) + }) + + Convey("there should be a 'not found' error returned", func() { + repository, _ := NewPostgresCNSIRepository(db) + _, err := repository.ListByAPIEndpoint(mockAPIEndpoint, mockEncryptionKey) + So(err, ShouldResemble, errors.New(expectedErrorMessage)) + + dberr := mock.ExpectationsWereMet() + So(dberr, ShouldBeNil) + }) + }) + }) + Convey("Given a request to find a specific CNSI by GUID", t, func() { db, mock, err := sqlmock.New() From 98b91f394f4100e01fcd7dff00f6175ab4c41db8 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 4 Mar 2021 10:42:20 +0100 Subject: [PATCH 082/101] Fix endpoint path (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jetstream/main.go b/src/jetstream/main.go index 4bbef050a3..f32d371fd9 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -1129,7 +1129,7 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, needSetupMiddleware bool) { // Use middleware in route directly, because documentation is faulty // Apply middleware to group with .Use() when this issue is resolved: // https://github.com/labstack/echo/issues/1519 - stableEndpointAdminAPIGroup.POST("/endpoints:id", p.updateEndpoint, p.endpointUpdateDeleteMiddleware) + stableEndpointAdminAPIGroup.POST("/endpoints/:id", p.updateEndpoint, p.endpointUpdateDeleteMiddleware) stableEndpointAdminAPIGroup.DELETE("/endpoints/:id", p.unregisterCluster, p.endpointUpdateDeleteMiddleware) } else { stableEndpointAdminAPIGroup.Use(p.adminMiddleware) From 3422bb2a5d1393c9132f55d3f311e5ba92febc37 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Thu, 4 Mar 2021 11:49:22 +0000 Subject: [PATCH 083/101] Minor tweaks - Added additional comments - Tidied up some parts - Display endpoint create directly rather than via custom component - Added new doc to website menu --- .../cf/user-invites/user-invite.service.ts | 9 +++-- .../create-endpoint-cf-step-1.component.ts | 6 ++-- ...component.ts => create-endpoint-helper.ts} | 13 +++---- .../create-endpoint-helper.component.spec.ts | 32 ----------------- .../create-endpoint/create-endpoint.module.ts | 3 -- .../endpoint/endpoint-list.helpers.ts | 23 ++++++------ .../endpoint/endpoints-list-config.service.ts | 21 +++++------ ...table-cell-endpoint-creator.component.html | 3 -- ...table-cell-endpoint-creator.component.scss | 0 ...le-cell-endpoint-creator.component.spec.ts | 35 ------------------- .../table-cell-endpoint-creator.component.ts | 30 ---------------- .../packages/core/src/shared/shared.module.ts | 2 -- .../git-registration.component.ts | 6 ++-- .../packages/store/src/types/auth.types.ts | 9 +++++ src/jetstream/cnsi.go | 8 +++-- src/jetstream/plugins/backup/main.go | 2 +- website/sidebars.js | 3 +- 17 files changed, 53 insertions(+), 152 deletions(-) rename src/frontend/packages/core/src/features/endpoints/create-endpoint/{create-endpoint-helper/create-endpoint-helper.component.ts => create-endpoint-helper.ts} (71%) delete mode 100644 src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper/create-endpoint-helper.component.spec.ts delete mode 100644 src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.html delete mode 100644 src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.scss delete mode 100644 src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.spec.ts delete mode 100644 src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.ts diff --git a/src/frontend/packages/cloud-foundry/src/features/cf/user-invites/user-invite.service.ts b/src/frontend/packages/cloud-foundry/src/features/cf/user-invites/user-invite.service.ts index 637a1cbd09..6ebb948cbc 100644 --- a/src/frontend/packages/cloud-foundry/src/features/cf/user-invites/user-invite.service.ts +++ b/src/frontend/packages/cloud-foundry/src/features/cf/user-invites/user-invite.service.ts @@ -143,12 +143,11 @@ export class UserInviteService { map(v => v.entity.metadata && v.entity.metadata.userInviteAllowed === 'true') ); - this.canConfigure$ = combineLatest([ + this.canConfigure$ = combineLatest( waitForCFPermissions(this.store, this.activeRouteCfOrgSpace.cfGuid), - this.store.select('auth'), - cfEndpointService.endpoint$ - ]).pipe( - map(([cf, auth, endpoint]) => + this.store.select('auth') + ).pipe( + map(([cf, auth]) => cf.global.isAdmin && auth.sessionData['plugin-config'] && auth.sessionData['plugin-config'].userInvitationsEnabled === 'true') ); diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts index e924b43185..b21d80f862 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts @@ -1,7 +1,6 @@ import { AfterContentInit, Component, Input } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; -import { StratosCurrentUserPermissions } from '../../../../core/permissions/stratos-user-permissions.checker'; import { Observable } from 'rxjs'; import { filter, map, pairwise } from 'rxjs/operators'; @@ -12,13 +11,14 @@ import { } from '../../../../../../store/src/entity-catalog/entity-catalog-entity/entity-catalog-entity'; import { ActionState } from '../../../../../../store/src/reducers/api-request-reducer/types'; import { stratosEntityCatalog } from '../../../../../../store/src/stratos-entity-catalog'; +import { StratosCurrentUserPermissions } from '../../../../core/permissions/stratos-user-permissions.checker'; import { getIdFromRoute } from '../../../../core/utils.service'; import { IStepperStep, StepOnNextFunction } from '../../../../shared/components/stepper/step/step.component'; +import { SessionService } from '../../../../shared/services/session.service'; import { SnackBarService } from '../../../../shared/services/snackbar.service'; import { ConnectEndpointConfig } from '../../connect.service'; import { getSSOClientRedirectURI } from '../../endpoint-helpers'; -import { SessionService } from '../../../../shared/services/session.service'; -import { CreateEndpointHelperComponent } from '../create-endpoint-helper/create-endpoint-helper.component'; +import { CreateEndpointHelperComponent } from '../create-endpoint-helper'; @Component({ diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper/create-endpoint-helper.component.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper.ts similarity index 71% rename from src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper/create-endpoint-helper.component.ts rename to src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper.ts index 08551dfbe5..a7d24ce0aa 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper/create-endpoint-helper.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper.ts @@ -1,21 +1,16 @@ -import { Component } from '@angular/core'; -import { StratosCurrentUserPermissions } from '../../../../core/permissions/stratos-user-permissions.checker'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { getFullEndpointApiUrl } from '../../../../../../store/src/endpoint-utils'; -import { stratosEntityCatalog } from '../../../../../../store/src/stratos-entity-catalog'; -import { SessionService } from '../../../../shared/services/session.service'; +import { getFullEndpointApiUrl } from '../../../../../store/src/endpoint-utils'; +import { stratosEntityCatalog } from '../../../../../store/src/stratos-entity-catalog'; +import { StratosCurrentUserPermissions } from '../../../core/permissions/stratos-user-permissions.checker'; +import { SessionService } from '../../../shared/services/session.service'; type EndpointObservable = Observable<{ names: string[], urls: string[], }>; -@Component({ - selector: 'app-create-endpoint-helper', - template: '' -}) export class CreateEndpointHelperComponent { overwritePermission: Observable; diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper/create-endpoint-helper.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper/create-endpoint-helper.component.spec.ts deleted file mode 100644 index 7ea0d22cbb..0000000000 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper/create-endpoint-helper.component.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { SessionService } from 'frontend/packages/core/src/shared/services/session.service'; -import { createBasicStoreModule } from '@stratosui/store/testing'; -import { CreateEndpointHelperComponent } from './create-endpoint-helper.component'; -import { CoreTestingModule } from '../../../../../test-framework/core-test.modules'; - -describe('CreateEndpointHelperComponent', () => { - let component: CreateEndpointHelperComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ CreateEndpointHelperComponent ], - imports: [ - CoreTestingModule, - createBasicStoreModule(), - ], - providers: [ SessionService ] - }) - .compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(CreateEndpointHelperComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.module.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.module.ts index 1505415ee2..f747ece74c 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.module.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.module.ts @@ -8,7 +8,6 @@ import { CreateEndpointBaseStepComponent } from './create-endpoint-base-step/cre import { CreateEndpointCfStep1Component } from './create-endpoint-cf-step-1/create-endpoint-cf-step-1.component'; import { CreateEndpointConnectComponent } from './create-endpoint-connect/create-endpoint-connect.component'; import { CreateEndpointComponent } from './create-endpoint.component'; -import { CreateEndpointHelperComponent } from './create-endpoint-helper/create-endpoint-helper.component'; @NgModule({ imports: [ @@ -22,14 +21,12 @@ import { CreateEndpointHelperComponent } from './create-endpoint-helper/create-e CreateEndpointBaseStepComponent, CreateEndpointConnectComponent, ConnectEndpointComponent, - CreateEndpointHelperComponent ], exports: [ CreateEndpointComponent, CreateEndpointCfStep1Component, CreateEndpointConnectComponent, ConnectEndpointComponent, - CreateEndpointHelperComponent ] }) export class CreateEndpointModule { } diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts index 5a8fa03b98..48b2a33208 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts @@ -1,4 +1,3 @@ -import { isNgTemplate } from '@angular/compiler'; import { ComponentFactoryResolver, ComponentRef, Injectable, ViewContainerRef } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; @@ -16,13 +15,13 @@ import { StratosCurrentUserPermissions } from '../../../../../core/permissions/s import { ConnectEndpointDialogComponent, } from '../../../../../features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component'; +import { SessionService } from '../../../../../shared/services/session.service'; import { SnackBarService } from '../../../../services/snackbar.service'; import { ConfirmationDialogConfig } from '../../../confirmation-dialog.config'; import { ConfirmationDialogService } from '../../../confirmation-dialog.service'; import { createMetaCardMenuItemSeparator } from '../../list-cards/meta-card/meta-card-base/meta-card.component'; import { IListAction } from '../../list.component.types'; import { TableCellCustom } from '../../list.types'; -import { SessionService } from '../../../../../shared/services/session.service'; interface EndpointDetailsContainerRefs { componentRef: ComponentRef; @@ -111,9 +110,9 @@ export class EndpointListHelper { label: 'Disconnect', description: ``, // Description depends on console user permission createVisible: (row$: Observable) => combineLatest([ - this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT), - row$ - ]).pipe( + this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT), + row$ + ]).pipe( map(([isAdmin, row]) => { const isConnected = row.connectionStatus === 'connected'; return isConnected && (!row.system_shared_token || row.system_shared_token && isAdmin); @@ -142,9 +141,11 @@ export class EndpointListHelper { row$ ]).pipe( map(([userEndpointsEnabled, isAdmin, row]) => { - if (userEndpointsEnabled && !row.creator.admin && isAdmin){ + if (userEndpointsEnabled && !row.creator.admin && isAdmin) { + // Disable connect for admins if the endpoint was not created by them. Otherwise this could result in an admin connecting to + // multiple user endpoints that all have the same url. return false; - }else{ + } else { const endpoint = entityCatalog.getEndpoint(row.cnsi_type, row.sub_type); const ep = endpoint ? endpoint.definition : { unConnectable: false }; return !ep.unConnectable && row.connectionStatus === 'disconnected'; @@ -178,9 +179,9 @@ export class EndpointListHelper { row$ ]).pipe( map(([userEndpointsEnabled, isAdmin, isEndpointAdmin, row]) => { - if (!userEndpointsEnabled || row.creator.admin){ + if (!userEndpointsEnabled || row.creator.admin) { return isAdmin; - }else{ + } else { return isEndpointAdmin || isAdmin; } }) @@ -202,9 +203,9 @@ export class EndpointListHelper { row$ ]).pipe( map(([userEndpointsEnabled, isAdmin, isEndpointAdmin, row]) => { - if (!userEndpointsEnabled || row.creator.admin){ + if (!userEndpointsEnabled || row.creator.admin) { return isAdmin; - }else{ + } else { return isEndpointAdmin || isAdmin; } }) diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts index 4c165becef..e1205627b4 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts @@ -1,7 +1,7 @@ -import { Injectable, OnDestroy } from '@angular/core'; +import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; -import { BehaviorSubject, combineLatest, of, Subscription } from 'rxjs'; -import { debounceTime, filter, map } from 'rxjs/operators'; +import { BehaviorSubject, combineLatest, of } from 'rxjs'; +import { debounceTime, filter, first, map } from 'rxjs/operators'; import { ListView } from '../../../../../../../store/src/actions/list.actions'; import { SetClientFilter } from '../../../../../../../store/src/actions/pagination.actions'; @@ -29,7 +29,6 @@ import { EndpointCardComponent } from './endpoint-card/endpoint-card.component'; import { EndpointListHelper } from './endpoint-list.helpers'; import { EndpointsDataSource } from './endpoints-data-source'; import { TableCellEndpointAddressComponent } from './table-cell-endpoint-address/table-cell-endpoint-address.component'; -import { TableCellEndpointCreatorComponent } from './table-cell-endpoint-creator/table-cell-endpoint-creator.component'; import { TableCellEndpointDetailsComponent } from './table-cell-endpoint-details/table-cell-endpoint-details.component'; import { TableCellEndpointNameComponent } from './table-cell-endpoint-name/table-cell-endpoint-name.component'; import { TableCellEndpointStatusComponent } from './table-cell-endpoint-status/table-cell-endpoint-status.component'; @@ -37,7 +36,7 @@ import { TableCellEndpointStatusComponent } from './table-cell-endpoint-status/t @Injectable() -export class EndpointsListConfigService implements IListConfig, OnDestroy { +export class EndpointsListConfigService implements IListConfig { cardComponent = EndpointCardComponent; private singleActions: IListAction[]; @@ -109,7 +108,6 @@ export class EndpointsListConfigService implements IListConfig, O filter: 'Filter Endpoints' }; enableTextFilter = true; - userEndpointsSubscription: Subscription; constructor( private store: Store, @@ -125,12 +123,14 @@ export class EndpointsListConfigService implements IListConfig, O (row: EndpointModel) => userFavoriteManager.getFavoriteEndpointFromEntity(row) ); this.columns.push(favoriteCell); - this.userEndpointsSubscription = sessionService.userEndpointsNotDisabled().subscribe(enabled => { - if (enabled){ + sessionService.userEndpointsNotDisabled().pipe(first()).subscribe(enabled => { + if (enabled) { this.columns.splice(4, 0, { columnId: 'creator', headerCell: () => 'Creator', - cellComponent: TableCellEndpointCreatorComponent, + cellDefinition: { + valuePath: 'creator.name' + }, sort: { type: 'sort', orderKey: 'creator', @@ -221,7 +221,4 @@ export class EndpointsListConfigService implements IListConfig, O } } - ngOnDestroy() { - this.userEndpointsSubscription.unsubscribe(); - } } diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.html b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.html deleted file mode 100644 index 550579e875..0000000000 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.html +++ /dev/null @@ -1,3 +0,0 @@ -
- {{ creator }} -
\ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.scss b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.spec.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.spec.ts deleted file mode 100644 index df3ba7cf20..0000000000 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { CoreModule } from '@stratosui/core'; -import { EndpointModel } from '@stratosui/store'; - -import { TableCellEndpointCreatorComponent } from './table-cell-endpoint-creator.component'; - -describe('TableCellEndpointCreatorComponent', () => { - let component: TableCellEndpointCreatorComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ TableCellEndpointCreatorComponent ], - imports: [ - CoreModule - ] - }) - .compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(TableCellEndpointCreatorComponent); - component = fixture.componentInstance; - component.row = { - creator: { - name: 'dummy' - } - } as EndpointModel; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.ts deleted file mode 100644 index 16e53eb05c..0000000000 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { EndpointModel, entityCatalog } from '@stratosui/store'; -import { TableCellCustom } from '../../../list.types'; - -@Component({ - selector: 'app-table-cell-endpoint-creator', - templateUrl: './table-cell-endpoint-creator.component.html', - styleUrls: ['./table-cell-endpoint-creator.component.scss'] -}) -export class TableCellEndpointCreatorComponent extends TableCellCustom implements OnInit { - - public creator = ''; - - @Input() - get row(): EndpointModel { - return super.row; - } - set row(row: EndpointModel) { - super.row = row; - } - - constructor() { - super(); - } - - ngOnInit(): void { - this.creator = this.row.creator.name; - } - -} diff --git a/src/frontend/packages/core/src/shared/shared.module.ts b/src/frontend/packages/core/src/shared/shared.module.ts index b6c54ff774..38a45ca14b 100644 --- a/src/frontend/packages/core/src/shared/shared.module.ts +++ b/src/frontend/packages/core/src/shared/shared.module.ts @@ -119,7 +119,6 @@ import { LongRunningOperationsService } from './services/long-running-op.service import { MetricsRangeSelectorService } from './services/metrics-range-selector.service'; import { SessionService } from './services/session.service'; import { UserPermissionDirective } from './user-permission.directive'; -import { TableCellEndpointCreatorComponent } from './components/list/list-types/endpoint/table-cell-endpoint-creator/table-cell-endpoint-creator.component'; @NgModule({ @@ -222,7 +221,6 @@ import { TableCellEndpointCreatorComponent } from './components/list/list-types/ CardProgressOverlayComponent, MaxListMessageComponent, ProfileSettingsComponent, - TableCellEndpointCreatorComponent, ], exports: [ ApplicationStateIconPipe, diff --git a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts index ddfb95dcf9..a4ed8d5c5d 100644 --- a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts +++ b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts @@ -1,14 +1,17 @@ import { Component, OnDestroy } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; +import { + CreateEndpointHelperComponent, +} from 'frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper'; import { Observable, Subscription } from 'rxjs'; import { filter, first, map, pairwise } from 'rxjs/operators'; -import { SessionService } from '../../../../../core/src/shared/services/session.service'; import { EndpointsService } from '../../../../../core/src/core/endpoints.service'; import { getIdFromRoute } from '../../../../../core/src/core/utils.service'; import { ConnectEndpointConfig } from '../../../../../core/src/features/endpoints/connect.service'; import { StepOnNextFunction } from '../../../../../core/src/shared/components/stepper/step/step.component'; +import { SessionService } from '../../../../../core/src/shared/services/session.service'; import { SnackBarService } from '../../../../../core/src/shared/services/snackbar.service'; import { getFullEndpointApiUrl } from '../../../../../store/src/endpoint-utils'; import { entityCatalog } from '../../../../../store/src/public-api'; @@ -16,7 +19,6 @@ import { ActionState } from '../../../../../store/src/reducers/api-request-reduc import { stratosEntityCatalog } from '../../../../../store/src/stratos-entity-catalog'; import { GIT_ENDPOINT_SUB_TYPES, GIT_ENDPOINT_TYPE } from '../../../store/git-entity-factory'; import { GitSCMService } from '../../scm/scm.service'; -import { CreateEndpointHelperComponent } from 'frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper/create-endpoint-helper.component'; interface EndpointSubTypes { [subType: string]: GithubTypes; diff --git a/src/frontend/packages/store/src/types/auth.types.ts b/src/frontend/packages/store/src/types/auth.types.ts index 619e5665cf..c12d9cd835 100644 --- a/src/frontend/packages/store/src/types/auth.types.ts +++ b/src/frontend/packages/store/src/types/auth.types.ts @@ -28,8 +28,17 @@ export enum APIKeysEnabled { ALL_USERS = 'all_users' } export enum UserEndpointsEnabled { + /** + * No users can see or create their own endpoints. Admins cannot see any previously created user endpoints. + */ DISABLED = 'disabled', + /** + * No users can see or create their own endpoints. Admins can manage previously created user endpoints + */ ADMIN_ONLY = 'admin_only', + /** + * Endpoint Admins can see and create their own endpoints. Admins can manage all user endpoints + */ ENABLED = 'enabled' } export interface SessionDataConfig { diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index bc1511a1ad..e9eb7a2db0 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -74,7 +74,7 @@ func (p *portalProxy) RegisterEndpoint(c echo.Context, fetchInfo interfaces.Info cnsiClientSecret = p.GetConfig().CFClientSecret } - uaaUserID, err := p.GetSessionStringValue(c, "user_id") + userID, err := p.GetSessionStringValue(c, "user_id") if err != nil { return interfaces.NewHTTPShadowError( http.StatusInternalServerError, @@ -82,7 +82,7 @@ func (p *portalProxy) RegisterEndpoint(c echo.Context, fetchInfo interfaces.Info "Failed to get session user: %v", err) } - newCNSI, err := p.DoRegisterEndpoint(params.CNSIName, params.APIEndpoint, skipSSLValidation, cnsiClientId, cnsiClientSecret, uaaUserID, ssoAllowed, subType, overwriteEndpoints, fetchInfo) + newCNSI, err := p.DoRegisterEndpoint(params.CNSIName, params.APIEndpoint, skipSSLValidation, cnsiClientId, cnsiClientSecret, userID, ssoAllowed, subType, overwriteEndpoints, fetchInfo) if err != nil { return err } @@ -171,6 +171,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk if isAdmin && overwriteEndpoints { for _, duplicate := range duplicateEndpoints { + log.Infof("An administrator is registering an endpoint with the same API URL ('%+v') as an endpoint administrator's. The existing duplicate endpoint ('$+v') will be removed", apiEndpoint, duplicate.GUID) err := p.doUnregisterCluster(duplicate.GUID) if err != nil { return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( @@ -187,6 +188,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk h := sha1.New() // see why its generated this way in Issue #4753 / #3031 if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled && !isAdmin { + // Make the new guid unique per api url AND user id h.Write([]byte(apiEndpointURL.String() + userId)) } else { h.Write([]byte(apiEndpointURL.String())) @@ -321,7 +323,7 @@ func (p *portalProxy) ListEndpoints() ([]*interfaces.CNSIRecord, error) { return cnsiList, nil } -//return a CNSI list with endpoints created by the current endpointadmin and all admins +// ListAdminEndpoints - return a CNSI list with endpoints created by the current user and all admins func (p *portalProxy) ListAdminEndpoints(userID string) ([]*interfaces.CNSIRecord, error) { log.Debug("ListAdminEndpoints") // Initialise cnsiList to ensure empty struct (marshals to null) is not returned diff --git a/src/jetstream/plugins/backup/main.go b/src/jetstream/plugins/backup/main.go index 72d23b4833..cf1032ea15 100644 --- a/src/jetstream/plugins/backup/main.go +++ b/src/jetstream/plugins/backup/main.go @@ -57,7 +57,7 @@ func (br *BackupRestore) AddSessionGroupRoutes(echoGroup *echo.Group) { func (br *BackupRestore) Init() error { enabledStr := br.portalProxy.Env().String("FEATURE_ALLOW_BACKUP", "true") if enabled, err := strconv.ParseBool(enabledStr); err == nil && !enabled { - return errors.New("Backup/restoure feature disabled via configuration") + return errors.New("Backup/restore feature disabled via configuration") } return nil diff --git a/website/sidebars.js b/website/sidebars.js index 9d38f14e6e..251a3d4a96 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -31,7 +31,8 @@ module.exports = { { 'Cloud Foundry': [ 'endpoints/cf/cf', - 'endpoints/cf/invite-user-guide' + 'endpoints/cf/invite-user-guide', + 'endpoints/cf/user-endpoints' ] }, 'endpoints/k8s', From e1fa11c0f6e5efd4be6e1f91511590a8212b1be6 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Thu, 4 Mar 2021 15:02:12 +0000 Subject: [PATCH 084/101] Small changes All of these are unlreated to this pr, but given their size and ease of reproducing them adding them here --- src/frontend/packages/core/src/jetstream.helpers.ts | 4 ---- .../packages/store/src/effects/endpoint.effects.ts | 10 ++++------ src/jetstream/main.go | 4 ++-- 3 files changed, 6 insertions(+), 12 deletions(-) delete mode 100644 src/frontend/packages/core/src/jetstream.helpers.ts diff --git a/src/frontend/packages/core/src/jetstream.helpers.ts b/src/frontend/packages/core/src/jetstream.helpers.ts deleted file mode 100644 index 7de9c29e1e..0000000000 --- a/src/frontend/packages/core/src/jetstream.helpers.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { HttpErrorResponse } from '@angular/common/http'; - -import { isHttpErrorResponse, JetStreamErrorResponse } from '../../store/src/jetstream'; - diff --git a/src/frontend/packages/store/src/effects/endpoint.effects.ts b/src/frontend/packages/store/src/effects/endpoint.effects.ts index f39b06c995..426bf173fb 100644 --- a/src/frontend/packages/store/src/effects/endpoint.effects.ts +++ b/src/frontend/packages/store/src/effects/endpoint.effects.ts @@ -253,9 +253,8 @@ export class EndpointsEffect { })); private processUpdateError(e: HttpErrorResponse): string { - const err = e.error ? e.error.error : {}; - let message = 'There was a problem updating the endpoint' + - `${err.error ? ' (' + err.error + ').' : ''}`; + let message = 'There was a problem updating the endpoint. ' + + httpErrorResponseToSafeString(e); if (e.status === 403) { message = `${message}. Please check \"Skip SSL validation for the endpoint\" if the certificate issuer is trusted`; } @@ -263,9 +262,8 @@ export class EndpointsEffect { } private processRegisterError(e: HttpErrorResponse): string { - let message = 'There was a problem creating the endpoint. ' + - `Please ensure the endpoint address is correct and try again` + - `${e.error.error ? ' (' + e.error.error + ').' : ''}`; + let message = 'There was a problem creating the endpoint. Please ensure the endpoint address is correct and try again. ' + + httpErrorResponseToSafeString(e); if (e.status === 403) { message = `${e.error.error}. Please check \"Skip SSL validation for the endpoint\" if the certificate issuer is trusted`; } diff --git a/src/jetstream/main.go b/src/jetstream/main.go index f32d371fd9..bcb1705263 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -689,13 +689,13 @@ func newPortalProxy(pc interfaces.PortalConfig, dcp *sql.DB, ss HttpSessionStore // Setting default value for APIKeysEnabled if pc.APIKeysEnabled == "" { - log.Debug(`APIKeysEnabled not set, setting to "admin_only"`) + log.Info(`APIKeysEnabled not set, setting to "admin_only"`) pc.APIKeysEnabled = config.APIKeysConfigEnum.AdminOnly } // Setting default value for UserEndpointsEnabled if pc.UserEndpointsEnabled == "" { - log.Debug(`UserEndpointsEnabled not set, setting to "disabled"`) + log.Info(`UserEndpointsEnabled not set, setting to "disabled"`) pc.UserEndpointsEnabled = config.UserEndpointsConfigEnum.Disabled } From 4de9dec1f1517754e4c15b6ba15b0aa897142847 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Fri, 5 Mar 2021 09:43:57 +0100 Subject: [PATCH 085/101] Dont allow user to edit cnsi when admin_only (4876) Signed-off-by: Thomas Quandt --- src/jetstream/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jetstream/main.go b/src/jetstream/main.go index bcb1705263..6ad5f6d3fd 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -1123,7 +1123,7 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, needSetupMiddleware bool) { // static html will be returned instead. That's why we use the path "" stableEndpointAdminAPIGroup := stableAdminAPIGroup.Group("") - if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled { + if p.GetConfig().UserEndpointsEnabled == config.UserEndpointsConfigEnum.Enabled { stableEndpointAdminAPIGroup.Use(p.endpointAdminMiddleware) stableEndpointAdminAPIGroup.POST("/endpoints", p.pluginRegisterRouter) // Use middleware in route directly, because documentation is faulty From da9bc2ca44422c8851ed4b67a20a71e087128052 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Fri, 5 Mar 2021 12:00:54 +0100 Subject: [PATCH 086/101] Don't let users see creator when admin_only (#4876) Signed-off-by: Thomas Quandt --- .../endpoint-card/endpoint-card.component.html | 2 +- .../endpoint-card/endpoint-card.component.ts | 17 ++++++++++++++--- .../endpoint/endpoints-list-config.service.ts | 13 ++++++++++++- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html index d00af9b383..cd9de79533 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.html @@ -44,7 +44,7 @@
- + Creator
{{ row.creator.name }}
diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.ts index 4849f8a167..0f002408dd 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.ts @@ -9,7 +9,7 @@ import { ViewContainerRef, } from '@angular/core'; import { Store } from '@ngrx/store'; -import { Observable, of, ReplaySubject, Subscription } from 'rxjs'; +import { combineLatest, Observable, of, ReplaySubject, Subscription } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; import { AppState } from '../../../../../../../../store/src/app-state'; @@ -33,6 +33,8 @@ import { EndpointListDetailsComponent, EndpointListHelper } from '../endpoint-li import { RouterNav } from './../../../../../../../../store/src/actions/router.actions'; import { CopyToClipboardComponent } from './../../../../copy-to-clipboard/copy-to-clipboard.component'; import { SessionService } from '../../../../../services/session.service'; +import { CurrentUserPermissionsService } from '../../../../../../core/permissions/current-user-permissions.service'; +import { StratosCurrentUserPermissions } from '../../../../../../core/permissions/stratos-user-permissions.checker'; @Component({ selector: 'app-endpoint-card', @@ -55,7 +57,7 @@ export class EndpointCardComponent extends CardCell implements On public cardStatus$: Observable; private subs: Subscription[] = []; public connectionStatus: string; - public enableUserEndpoints$: Observable; + public viewCreator$: Observable; private componentRef: ComponentRef; @@ -123,6 +125,7 @@ export class EndpointCardComponent extends CardCell implements On private endpointListHelper: EndpointListHelper, private componentFactoryResolver: ComponentFactoryResolver, private userFavoriteManager: UserFavoriteManager, + private currentUserPermissionsService: CurrentUserPermissionsService, private sessionService: SessionService, ) { super(); @@ -133,7 +136,15 @@ export class EndpointCardComponent extends CardCell implements On this.favorite = this.userFavoriteManager.getFavoriteEndpointFromEntity(this.row); const e = this.endpointCatalogEntity.definition; this.hasDetails = !!e && !!e.listDetailsComponent; - this.enableUserEndpoints$ = this.sessionService.userEndpointsNotDisabled(); + this.viewCreator$ = combineLatest([ + this.sessionService.userEndpointsEnabled(), + this.sessionService.userEndpointsNotDisabled(), + this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT), + ]).pipe( + map(([userEndpointsEnabled, userEndpointsNotDisabled, isAdmin]) => { + return userEndpointsEnabled || (userEndpointsNotDisabled && isAdmin); + }) + ); } ngOnDestroy(): void { diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts index e1205627b4..53eea98d8e 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts @@ -15,6 +15,8 @@ import { EndpointModel } from '../../../../../../../store/src/types/endpoint.typ import { PaginationEntityState } from '../../../../../../../store/src/types/pagination.types'; import { UserFavoriteManager } from '../../../../../../../store/src/user-favorite-manager'; import { SessionService } from '../../../../services/session.service'; +import { CurrentUserPermissionsService } from '../../../../../core/permissions/current-user-permissions.service'; +import { StratosCurrentUserPermissions } from '../../../../../core/permissions/stratos-user-permissions.checker'; import { createTableColumnFavorite } from '../../list-table/table-cell-favorite/table-cell-favorite.component'; import { ITableColumn } from '../../list-table/table.types'; import { @@ -116,6 +118,7 @@ export class EndpointsListConfigService implements IListConfig { internalEventMonitorFactory: InternalEventMonitorFactory, endpointListHelper: EndpointListHelper, userFavoriteManager: UserFavoriteManager, + currentUserPermissionsService: CurrentUserPermissionsService, sessionService: SessionService ) { this.singleActions = endpointListHelper.endpointActions(); @@ -123,7 +126,15 @@ export class EndpointsListConfigService implements IListConfig { (row: EndpointModel) => userFavoriteManager.getFavoriteEndpointFromEntity(row) ); this.columns.push(favoriteCell); - sessionService.userEndpointsNotDisabled().pipe(first()).subscribe(enabled => { + combineLatest([ + sessionService.userEndpointsEnabled(), + sessionService.userEndpointsNotDisabled(), + currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT), + ]).pipe( + map(([userEndpointsEnabled, userEndpointsNotDisabled, isAdmin]) => { + return userEndpointsEnabled || (userEndpointsNotDisabled && isAdmin); + }) + ).subscribe(enabled => { if (enabled) { this.columns.splice(4, 0, { columnId: 'creator', From 9f0ed4d27756991807aecf2557a83f29f107fa1a Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Fri, 5 Mar 2021 14:48:33 +0100 Subject: [PATCH 087/101] Fix log bug (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/cnsi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index e9eb7a2db0..904e61ad9f 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -171,7 +171,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk if isAdmin && overwriteEndpoints { for _, duplicate := range duplicateEndpoints { - log.Infof("An administrator is registering an endpoint with the same API URL ('%+v') as an endpoint administrator's. The existing duplicate endpoint ('$+v') will be removed", apiEndpoint, duplicate.GUID) + log.Infof("An administrator is registering an endpoint with the same API URL ('%+v') as an endpoint administrator's. The existing duplicate endpoint ('%+v') will be removed", apiEndpoint, duplicate.GUID) err := p.doUnregisterCluster(duplicate.GUID) if err != nil { return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( From 0f13b56b7cac4312d0da336d8f1a77873d4da71e Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Fri, 5 Mar 2021 15:25:21 +0100 Subject: [PATCH 088/101] Add USER_ENDPOINTS_ENABLED to helm chart values and documentation (#4876) Signed-off-by: Thomas Quandt --- deploy/kubernetes/console/README.md | 1 + deploy/kubernetes/console/templates/deployment.yaml | 2 ++ deploy/kubernetes/console/values.schema.json | 5 +++++ deploy/kubernetes/console/values.yaml | 3 +++ website/docs/deploy/kubernetes/install.md | 1 + website/docs/endpoints/cf/user-endpoints.md | 4 ++-- 6 files changed, 14 insertions(+), 2 deletions(-) diff --git a/deploy/kubernetes/console/README.md b/deploy/kubernetes/console/README.md index ad042ec99f..7cf0bf36ff 100644 --- a/deploy/kubernetes/console/README.md +++ b/deploy/kubernetes/console/README.md @@ -78,6 +78,7 @@ The following table lists the configurable parameters of the Stratos Helm chart |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 (disabled, admin_only, all_users)|admin_only| +|console.userEndpointsEnabled|Enable/disable user endpoints or let only admins view and manage user endpoints (disabled, admin_only, enabled)|disabled| |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 3faa48f2b6..485c084ae5 100644 --- a/deploy/kubernetes/console/templates/deployment.yaml +++ b/deploy/kubernetes/console/templates/deployment.yaml @@ -288,6 +288,8 @@ spec: value: {{ default "false" .Values.console.techPreview | quote }} - name: API_KEYS_ENABLED value: {{ default "admin_only" .Values.console.apiKeysEnabled | quote }} + - name: USER_ENDPOINTS_ENABLED + value: {{ default "disabled" .Values.console.userEndpointsEnabled | quote }} - name: HELM_CACHE_FOLDER value: /helm-cache {{- if .Values.console.ui }} diff --git a/deploy/kubernetes/console/values.schema.json b/deploy/kubernetes/console/values.schema.json index 17c7ddeb6d..52c39f5ec8 100644 --- a/deploy/kubernetes/console/values.schema.json +++ b/deploy/kubernetes/console/values.schema.json @@ -14,6 +14,11 @@ "enum": ["disabled", "admin_only", "all_users"], "description": "Enable API keys for admins, all users or nobody" }, + "userEndpointsEnabled": { + "type": "string", + "enum": ["disabled", "admin_only", "enabled"], + "description": "Enable, disable or let only admins view user endpoints" + }, "autoRegisterCF": { "type": ["string", "null"] }, diff --git a/deploy/kubernetes/console/values.yaml b/deploy/kubernetes/console/values.yaml index 5c7b537ede..a3260da2b0 100644 --- a/deploy/kubernetes/console/values.yaml +++ b/deploy/kubernetes/console/values.yaml @@ -70,6 +70,9 @@ console: # Enable/disable API key-based access to Stratos API apiKeysEnabled: admin_only + # Enable/disable user endpoints + userEndpointsEnabled: disabled + 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: diff --git a/website/docs/deploy/kubernetes/install.md b/website/docs/deploy/kubernetes/install.md index fad7b188d4..1417667a44 100644 --- a/website/docs/deploy/kubernetes/install.md +++ b/website/docs/deploy/kubernetes/install.md @@ -79,6 +79,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.userEndpointsEnabled|Enable/disable user endpoints or let only admins view and manage user endpoints (disabled, admin_only, enabled)|disabled| |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/website/docs/endpoints/cf/user-endpoints.md b/website/docs/endpoints/cf/user-endpoints.md index aea30ee546..5c7b542000 100644 --- a/website/docs/endpoints/cf/user-endpoints.md +++ b/website/docs/endpoints/cf/user-endpoints.md @@ -11,7 +11,7 @@ Stratos provides a way for users to create endpoints without the need to be an a In order to enable User Endpoints support in Stratos: -1. The environment variable `USER_ENDPOINTS_ENABLED` must be set +1. The environment variable `USER_ENDPOINTS_ENABLED` or helm chart value `console.userEndpointsEnabled` must be set 2. The UAA client used by Stratos needs an additional scope `stratos.endpointadmin` 3. Users need to have the `stratos.endpointadmin` group attached to them @@ -19,7 +19,7 @@ Once all steps have been completed, user within the `stratos.endpointadmin` grou ## Environment variable -`USER_ENDPOINTS_ENABLED` can be set to three different states: +`USER_ENDPOINTS_ENABLED` or helm chart value `console.userEndpointsEnabled` can be set to three different states: 1. `disabled` (default) will disable this feature. Neither admins nor users will see user endpoints. 2. `admin_only` will hide user endpoints from users. Admins can still see all endpoints created by users. From 9b3fd3e86cd9e2c66decc154147ba6a788d8de8d Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Wed, 10 Mar 2021 09:24:41 +0100 Subject: [PATCH 089/101] Add comment and clean up code (#4876) Signed-off-by: Thomas Quandt --- .../list/list-types/endpoint/endpoint-list.helpers.ts | 2 ++ src/jetstream/cnsi_test.go | 6 ------ 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts index 48b2a33208..1954136782 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts @@ -172,6 +172,8 @@ export class EndpointListHelper { label: 'Unregister', description: 'Remove the endpoint', createVisible: (row$: Observable) => { + // I think if we end up using these often there should be specific create, + // edit, delete style permissions in the stratos permissions checker return combineLatest([ this.sessionService.userEndpointsNotDisabled(), this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT), diff --git a/src/jetstream/cnsi_test.go b/src/jetstream/cnsi_test.go index 620923de0f..635a8f08be 100644 --- a/src/jetstream/cnsi_test.go +++ b/src/jetstream/cnsi_test.go @@ -570,12 +570,6 @@ func TestRegisterWithUserEndpointsEnabled(t *testing.T) { }) }) - - Convey("with UserEndpointsEnabled=disabled", func() { - Convey("register existing user endpoint", func() { - // todo user endpoints should automatically overwritten? - }) - }) }) } From b26ccb083a28269de17e86ee9d8fbaa7b63cc1ec Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Wed, 31 Mar 2021 14:18:35 +0200 Subject: [PATCH 090/101] Admin and users can create endpoint with same url (#4876) Signed-off-by: Thomas Quandt --- .../create-endpoint-cf-step-1.component.html | 6 +-- .../create-endpoint-cf-step-1.component.ts | 26 ++-------- .../create-endpoint/create-endpoint-helper.ts | 51 +++++++++++++++---- .../git-registration.component.html | 6 +-- .../git-registration.component.ts | 4 +- src/jetstream/cnsi.go | 12 +++-- 6 files changed, 62 insertions(+), 43 deletions(-) diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html index 054718e20d..6d320c70d6 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html @@ -2,19 +2,19 @@

{{endpoint.definition.label}} Information

+ [appUnique]="(customEndpoints | async)?.names"> Name is required Name is not unique + [appUnique]="(customEndpoints | async)?.urls"> URL is required Invalid API URL URL is not unique - Automatically overwrite user endpoints diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts index b21d80f862..c7c53fd426 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts @@ -4,17 +4,16 @@ import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { filter, map, pairwise } from 'rxjs/operators'; -import { getFullEndpointApiUrl } from '../../../../../../store/src/endpoint-utils'; import { entityCatalog } from '../../../../../../store/src/entity-catalog/entity-catalog'; import { StratosCatalogEndpointEntity, } from '../../../../../../store/src/entity-catalog/entity-catalog-entity/entity-catalog-entity'; import { ActionState } from '../../../../../../store/src/reducers/api-request-reducer/types'; import { stratosEntityCatalog } from '../../../../../../store/src/stratos-entity-catalog'; -import { StratosCurrentUserPermissions } from '../../../../core/permissions/stratos-user-permissions.checker'; import { getIdFromRoute } from '../../../../core/utils.service'; import { IStepperStep, StepOnNextFunction } from '../../../../shared/components/stepper/step/step.component'; import { SessionService } from '../../../../shared/services/session.service'; +import { CurrentUserPermissionsService } from '../../../../core/permissions/current-user-permissions.service'; import { SnackBarService } from '../../../../shared/services/snackbar.service'; import { ConnectEndpointConfig } from '../../connect.service'; import { getSSOClientRedirectURI } from '../../endpoint-helpers'; @@ -63,9 +62,10 @@ export class CreateEndpointCfStep1Component extends CreateEndpointHelperComponen private fb: FormBuilder, activatedRoute: ActivatedRoute, private snackBarService: SnackBarService, - sessionService: SessionService + sessionService: SessionService, + currentUserPermissionsService: CurrentUserPermissionsService ) { - super(sessionService); + super(sessionService, currentUserPermissionsService); this.registerForm = this.fb.group({ nameField: ['', [Validators.required]], @@ -78,24 +78,6 @@ export class CreateEndpointCfStep1Component extends CreateEndpointHelperComponen overwriteEndpointsField: [false, []], }); - const currentPage$ = stratosEntityCatalog.endpoint.store.getAll.getPaginationMonitor().currentPage$; - this.existingAdminEndpoints = currentPage$.pipe( - map(endpoints => ({ - names: endpoints.filter(ep => ep.creator.admin).map(ep => ep.name), - urls: endpoints.filter(ep => ep.creator.admin).map(ep => getFullEndpointApiUrl(ep)), - })) - ); - this.existingEndpoints = currentPage$.pipe( - map(endpoints => ({ - names: endpoints.map(ep => ep.name), - urls: endpoints.map(ep => getFullEndpointApiUrl(ep)), - })) - ); - - this.overwritePermission = this.sessionService.userEndpointsNotDisabled().pipe( - map(enabled => enabled ? [StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT] : []) - ); - const epType = getIdFromRoute(activatedRoute, 'type'); const epSubType = getIdFromRoute(activatedRoute, 'subtype'); this.endpoint = entityCatalog.getEndpoint(epType, epSubType); diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper.ts index a7d24ce0aa..d198b839c6 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper.ts @@ -1,8 +1,9 @@ -import { Observable } from 'rxjs'; +import { Observable, combineLatest } from 'rxjs'; import { map } from 'rxjs/operators'; import { getFullEndpointApiUrl } from '../../../../../store/src/endpoint-utils'; import { stratosEntityCatalog } from '../../../../../store/src/stratos-entity-catalog'; +import { CurrentUserPermissionsService } from '../../../core/permissions/current-user-permissions.service'; import { StratosCurrentUserPermissions } from '../../../core/permissions/stratos-user-permissions.checker'; import { SessionService } from '../../../shared/services/session.service'; @@ -13,29 +14,61 @@ type EndpointObservable = Observable<{ export class CreateEndpointHelperComponent { - overwritePermission: Observable; - existingEndpoints: EndpointObservable; - existingAdminEndpoints: EndpointObservable; + userEndpointsAndIsAdmin: Observable; + customEndpoints: EndpointObservable; constructor( - public sessionService: SessionService + public sessionService: SessionService, + public currentUserPermissionsService: CurrentUserPermissionsService ) { const currentPage$ = stratosEntityCatalog.endpoint.store.getAll.getPaginationMonitor().currentPage$; - this.existingAdminEndpoints = currentPage$.pipe( + const existingAdminEndpoints = currentPage$.pipe( map(endpoints => ({ names: endpoints.filter(ep => ep.creator.admin).map(ep => ep.name), urls: endpoints.filter(ep => ep.creator.admin).map(ep => getFullEndpointApiUrl(ep)), })) ); - this.existingEndpoints = currentPage$.pipe( + const existingUserEndpoints = currentPage$.pipe( + map(endpoints => ({ + names: endpoints.filter(ep => !ep.creator.admin).map(ep => ep.name), + urls: endpoints.filter(ep => !ep.creator.admin).map(ep => getFullEndpointApiUrl(ep)), + })) + ); + const existingEndpoints = currentPage$.pipe( map(endpoints => ({ names: endpoints.map(ep => ep.name), urls: endpoints.map(ep => getFullEndpointApiUrl(ep)), })) ); - this.overwritePermission = this.sessionService.userEndpointsNotDisabled().pipe( - map(enabled => enabled ? [StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT] : []) + const isAdmin = this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT); + const userEndpointsNotDisabled = this.sessionService.userEndpointsNotDisabled(); + + this.userEndpointsAndIsAdmin = combineLatest([ + isAdmin, + userEndpointsNotDisabled + ]).pipe( + map(([admin, userEndpoints]) => admin && userEndpoints) ); + + this.customEndpoints = combineLatest([ + userEndpointsNotDisabled, + isAdmin, + existingEndpoints, + existingAdminEndpoints, + existingUserEndpoints + ]).pipe( + map(([userEndpointsEnabled, admin, endpoints, adminEndpoints, userEndpoints]) => { + if (userEndpointsEnabled){ + if (admin){ + return adminEndpoints; + }else{ + return userEndpoints; + } + } + return endpoints; + }) + ); + } } diff --git a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.html b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.html index bf3469013f..085697fcbd 100644 --- a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.html +++ b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.html @@ -19,19 +19,19 @@

Select the type of {{gitTypes[epSubType].label}} to register

+ [appUnique]="(customEndpoints | async)?.names"> Name is required Name is not unique + [appUnique]="(customEndpoints | async)?.urls"> URL is required Invalid API URL URL is not unique - Automatically overwrite user endpoints diff --git a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts index a4ed8d5c5d..27d1243780 100644 --- a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts +++ b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts @@ -12,6 +12,7 @@ import { getIdFromRoute } from '../../../../../core/src/core/utils.service'; import { ConnectEndpointConfig } from '../../../../../core/src/features/endpoints/connect.service'; import { StepOnNextFunction } from '../../../../../core/src/shared/components/stepper/step/step.component'; import { SessionService } from '../../../../../core/src/shared/services/session.service'; +import { CurrentUserPermissionsService } from '../../../../../core/src/core/permissions/current-user-permissions.service'; import { SnackBarService } from '../../../../../core/src/shared/services/snackbar.service'; import { getFullEndpointApiUrl } from '../../../../../store/src/endpoint-utils'; import { entityCatalog } from '../../../../../store/src/public-api'; @@ -76,8 +77,9 @@ export class GitRegistrationComponent extends CreateEndpointHelperComponent impl private snackBarService: SnackBarService, private endpointsService: EndpointsService, public sessionService: SessionService, + public currentUserPermissionsService: CurrentUserPermissionsService ) { - super(sessionService); + super(sessionService, currentUserPermissionsService); this.epSubType = getIdFromRoute(activatedRoute, 'subtype'); const githubLabel = entityCatalog.getEndpoint(GIT_ENDPOINT_TYPE, GIT_ENDPOINT_SUB_TYPES.GITHUB).definition.label || 'Github'; const gitlabLabel = entityCatalog.getEndpoint(GIT_ENDPOINT_TYPE, GIT_ENDPOINT_SUB_TYPES.GITLAB).definition.label || 'Gitlab'; diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 904e61ad9f..d0304f8cd6 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -149,18 +149,18 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk } // check if we've already got this APIEndpoint in this DB for _, duplicate := range duplicateEndpoints { - // cant create same admin endpoint - if len(duplicate.Creator) == 0 { + // cant create same system endpoint + if len(duplicate.Creator) == 0 && isAdmin { return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( http.StatusBadRequest, - "Can not register same admin endpoint multiple times", - "Can not register same admin endpoint multiple times", + "Can not register same system endpoint multiple times", + "Can not register same system endpoint multiple times", ) } // cant create same user endpoint // can create same user endpoint if overwriteEndpoint true - if duplicate.Creator == userId || isAdmin && !overwriteEndpoints { + if duplicate.Creator == userId { return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( http.StatusBadRequest, "Can not register same endpoint multiple times", @@ -169,6 +169,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk } } + /* if isAdmin && overwriteEndpoints { for _, duplicate := range duplicateEndpoints { log.Infof("An administrator is registering an endpoint with the same API URL ('%+v') as an endpoint administrator's. The existing duplicate endpoint ('%+v') will be removed", apiEndpoint, duplicate.GUID) @@ -182,6 +183,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk } } } + */ } From baa0bfce5317ee2a9d8081aea629df632c0ce752 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Wed, 31 Mar 2021 16:39:16 +0200 Subject: [PATCH 091/101] Userendpoints hide system endpoints (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/cnsi.go | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index d0304f8cd6..9c89551ff7 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -301,7 +301,35 @@ func (p *portalProxy) buildCNSIList(c echo.Context) ([]*interfaces.CNSIRecord, e } if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.AdminOnly { - return p.ListAdminEndpoints(userID.(string)) + // remove existing system endpoint if user endpoint already exists and sessionuser not admin + unfilteredList, err := p.ListAdminEndpoints(userID.(string)) + if err != nil { + return unfilteredList, err + } + + filteredList := []*interfaces.CNSIRecord{} + + for _, endpoint := range unfilteredList { + duplicateSystemEndpoint := false + duplicateEndpointIndex := -1 + + for i := 0; i < len(filteredList); i++ { + if filteredList[i].APIEndpoint.String() == endpoint.APIEndpoint.String() { + duplicateSystemEndpoint = len(filteredList[i].Creator) != 0 + duplicateEndpointIndex = i + } + } + + if duplicateEndpointIndex != -1 && !u.Admin { + if duplicateSystemEndpoint { + filteredList[duplicateEndpointIndex] = endpoint + } + } else { + filteredList = append(filteredList, endpoint) + } + } + + return filteredList, err } } return p.ListAdminEndpoints("") From 18add2611799c9907435e6ad0e70e896b7f1c3d7 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 1 Apr 2021 14:04:49 +0200 Subject: [PATCH 092/101] Rename overwriteEndpoints in createUserEndpoint (#4876) Signed-off-by: Thomas Quandt --- .../create-endpoint-cf-step-1.component.html | 6 ++-- .../create-endpoint-cf-step-1.component.ts | 6 ++-- .../git-registration.component.html | 6 ++-- .../git-registration.component.ts | 8 ++--- .../store/src/actions/endpoint.actions.ts | 2 +- .../store/src/effects/endpoint.effects.ts | 2 +- .../store/src/stratos-action-builders.ts | 6 ++-- src/jetstream/cnsi.go | 32 +++++++++---------- src/jetstream/mock_server_test.go | 4 +-- .../repository/interfaces/portal_proxy.go | 2 +- .../repository/interfaces/structs.go | 2 +- 11 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html index 6d320c70d6..07c3a7c620 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html @@ -14,9 +14,9 @@

{{endpoint.definition.label}} Informa Invalid API URL URL is not unique - Automatically overwrite user endpoints + Create a user endpoint (only visible to you and other admins) Skip SSL validation for the diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts index c7c53fd426..5bd40be54b 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts @@ -75,7 +75,7 @@ export class CreateEndpointCfStep1Component extends CreateEndpointHelperComponen // Optional Client ID and Client Secret clientIDField: ['', []], clientSecretField: ['', []], - overwriteEndpointsField: [false, []], + createUserEndpointField: [false, []], }); const epType = getIdFromRoute(activatedRoute, 'type'); @@ -98,7 +98,7 @@ export class CreateEndpointCfStep1Component extends CreateEndpointHelperComponen this.registerForm.value.clientIDField, this.registerForm.value.clientSecretField, this.registerForm.value.ssoAllowedField, - this.registerForm.value.overwriteEndpointsField, + this.registerForm.value.createUserEndpointField, ).pipe( pairwise(), filter(([oldVal, newVal]) => (oldVal.busy && !newVal.busy)), @@ -149,7 +149,7 @@ export class CreateEndpointCfStep1Component extends CreateEndpointHelperComponen this.showAdvancedOptions = !this.showAdvancedOptions; } - toggleOverwriteEndpoints() { + toggleCreateUserEndpoint() { // wait a tick for validators to adjust to new data in the directive setTimeout(() => { this.registerForm.controls.nameField.updateValueAndValidity(); diff --git a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.html b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.html index 085697fcbd..21572423a8 100644 --- a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.html +++ b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.html @@ -31,9 +31,9 @@

Select the type of {{gitTypes[epSubType].label}} to register

Invalid API URL URL is not unique - Automatically overwrite user endpoints + Create a user endpoint (only visible to you and other admins) Skip SSL validation for the diff --git a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts index 27d1243780..bfdbf8c956 100644 --- a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts +++ b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts @@ -159,7 +159,7 @@ export class GitRegistrationComponent extends CreateEndpointHelperComponent impl nameField: ['', [Validators.required]], urlField: ['', [Validators.required]], skipSllField: [false, []], - overwriteEndpointsField: [false, []], + createUserEndpointField: [false, []], }); this.updateType(); @@ -201,10 +201,10 @@ export class GitRegistrationComponent extends CreateEndpointHelperComponent impl const skipSSL = this.registerForm.controls.nameField.value && this.registerForm.controls.urlField.value ? this.registerForm.controls.skipSllField.value : false; - const overwriteEndpoints = this.registerForm.controls.overwriteEndpointsField.value; + const createUserEndpoint = this.registerForm.controls.createUserEndpointField.value; return stratosEntityCatalog.endpoint.api.register(GIT_ENDPOINT_TYPE, - this.epSubType, name, url, skipSSL, '', '', false, overwriteEndpoints) + this.epSubType, name, url, skipSSL, '', '', false, createUserEndpoint) .pipe( pairwise(), filter(([oldVal, newVal]) => (oldVal.busy && !newVal.busy)), @@ -240,7 +240,7 @@ export class GitRegistrationComponent extends CreateEndpointHelperComponent impl return ready + '/' + defn.urlSuffix; } - toggleOverwriteEndpoints() { + toggleCreateUserEndpoint() { // wait a tick for validators to adjust to new data in the directive setTimeout(() => { this.registerForm.controls.nameField.updateValueAndValidity(); diff --git a/src/frontend/packages/store/src/actions/endpoint.actions.ts b/src/frontend/packages/store/src/actions/endpoint.actions.ts index 26b11a231a..2d93409c39 100644 --- a/src/frontend/packages/store/src/actions/endpoint.actions.ts +++ b/src/frontend/packages/store/src/actions/endpoint.actions.ts @@ -215,7 +215,7 @@ export class RegisterEndpoint extends SingleBaseEndpointAction { public clientID = '', public clientSecret = '', public ssoAllowed: boolean, - public overwriteEndpoints: boolean, + public createUserEndpoint: boolean, ) { super( REGISTER_ENDPOINTS, diff --git a/src/frontend/packages/store/src/effects/endpoint.effects.ts b/src/frontend/packages/store/src/effects/endpoint.effects.ts index 426bf173fb..e51ef30fa2 100644 --- a/src/frontend/packages/store/src/effects/endpoint.effects.ts +++ b/src/frontend/packages/store/src/effects/endpoint.effects.ts @@ -195,7 +195,7 @@ export class EndpointsEffect { cnsi_client_id: action.clientID, cnsi_client_secret: action.clientSecret, sso_allowed: action.ssoAllowed ? 'true' : 'false', - overwrite_endpoints: action.overwriteEndpoints ? 'true' : 'false' + create_user_endpoint: action.createUserEndpoint ? 'true' : 'false' }; // Do not include sub_type in HttpParams if it doesn't exist (falsies get stringified and sent) if (action.endpointSubType) { diff --git a/src/frontend/packages/store/src/stratos-action-builders.ts b/src/frontend/packages/store/src/stratos-action-builders.ts index d2c869fe1f..57d2dfe19b 100644 --- a/src/frontend/packages/store/src/stratos-action-builders.ts +++ b/src/frontend/packages/store/src/stratos-action-builders.ts @@ -60,7 +60,7 @@ export interface EndpointActionBuilder extends OrchestratedActionBuilders { clientID?: string, clientSecret?: string, ssoAllowed?: boolean, - overwriteEndpointsField?: boolean, + createUserEndpointField?: boolean, ) => RegisterEndpoint; update: ( guid: string, @@ -105,7 +105,7 @@ export const endpointActionBuilder: EndpointActionBuilder = { clientID?: string, clientSecret?: string, ssoAllowed?: boolean, - overwriteEndpoints?: boolean, + createUserEndpoint?: boolean, ) => new RegisterEndpoint( endpointType, endpointSubType, @@ -115,7 +115,7 @@ export const endpointActionBuilder: EndpointActionBuilder = { clientID, clientSecret, ssoAllowed, - overwriteEndpoints, + createUserEndpoint, ), update: ( guid: string, diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 9c89551ff7..ac6159ca67 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -63,10 +63,10 @@ func (p *portalProxy) RegisterEndpoint(c echo.Context, fetchInfo interfaces.Info cnsiClientSecret := params.CNSIClientSecret subType := params.SubType - overwriteEndpoints, err := strconv.ParseBool(params.OverwriteEndpoints) + createUserEndpoint, err := strconv.ParseBool(params.CreateUserEndpoint) if err != nil { // default to false - overwriteEndpoints = false + createUserEndpoint = false } if cnsiClientId == "" { @@ -82,7 +82,7 @@ func (p *portalProxy) RegisterEndpoint(c echo.Context, fetchInfo interfaces.Info "Failed to get session user: %v", err) } - newCNSI, err := p.DoRegisterEndpoint(params.CNSIName, params.APIEndpoint, skipSSLValidation, cnsiClientId, cnsiClientSecret, userID, ssoAllowed, subType, overwriteEndpoints, fetchInfo) + newCNSI, err := p.DoRegisterEndpoint(params.CNSIName, params.APIEndpoint, skipSSLValidation, cnsiClientId, cnsiClientSecret, userID, ssoAllowed, subType, createUserEndpoint, fetchInfo) if err != nil { return err } @@ -91,7 +91,7 @@ func (p *portalProxy) RegisterEndpoint(c echo.Context, fetchInfo interfaces.Info return nil } -func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, skipSSLValidation bool, clientId string, clientSecret string, userId string, ssoAllowed bool, subType string, overwriteEndpoints bool, fetchInfo interfaces.InfoFunc) (interfaces.CNSIRecord, error) { +func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, skipSSLValidation bool, clientId string, clientSecret string, userId string, ssoAllowed bool, subType string, createUserEndpoint bool, fetchInfo interfaces.InfoFunc) (interfaces.CNSIRecord, error) { log.Debug("DoRegisterEndpoint") if len(cnsiName) == 0 || len(apiEndpoint) == 0 { @@ -170,19 +170,19 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk } /* - if isAdmin && overwriteEndpoints { - for _, duplicate := range duplicateEndpoints { - log.Infof("An administrator is registering an endpoint with the same API URL ('%+v') as an endpoint administrator's. The existing duplicate endpoint ('%+v') will be removed", apiEndpoint, duplicate.GUID) - err := p.doUnregisterCluster(duplicate.GUID) - if err != nil { - return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( - http.StatusInternalServerError, - "Failed to unregister cluster", - "Failed to unregister cluster: %v", - err) + if isAdmin && overwriteEndpoints { + for _, duplicate := range duplicateEndpoints { + log.Infof("An administrator is registering an endpoint with the same API URL ('%+v') as an endpoint administrator's. The existing duplicate endpoint ('%+v') will be removed", apiEndpoint, duplicate.GUID) + err := p.doUnregisterCluster(duplicate.GUID) + if err != nil { + return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( + http.StatusInternalServerError, + "Failed to unregister cluster", + "Failed to unregister cluster: %v", + err) + } } } - } */ } @@ -315,7 +315,7 @@ func (p *portalProxy) buildCNSIList(c echo.Context) ([]*interfaces.CNSIRecord, e for i := 0; i < len(filteredList); i++ { if filteredList[i].APIEndpoint.String() == endpoint.APIEndpoint.String() { - duplicateSystemEndpoint = len(filteredList[i].Creator) != 0 + duplicateSystemEndpoint = len(filteredList[i].Creator) == 0 duplicateEndpointIndex = i } } diff --git a/src/jetstream/mock_server_test.go b/src/jetstream/mock_server_test.go index 58d1dfeec8..ec4c9e2cf6 100644 --- a/src/jetstream/mock_server_test.go +++ b/src/jetstream/mock_server_test.go @@ -272,7 +272,7 @@ func setupMockUser(guid string, admin bool, scopes []string) MockUser { } // mockV2Info needs to be closed -func setupMockEndpointRegisterRequest(t *testing.T, user *interfaces.ConnectedUser, mockV2Info *httptest.Server, endpointName string, overwriteEndpoints bool) MockEndpointRequest { +func setupMockEndpointRegisterRequest(t *testing.T, user *interfaces.ConnectedUser, mockV2Info *httptest.Server, endpointName string, createUserEndpoint bool) MockEndpointRequest { // create a request for each endpoint req := setupMockReq("POST", "", map[string]string{ @@ -281,7 +281,7 @@ func setupMockEndpointRegisterRequest(t *testing.T, user *interfaces.ConnectedUs "skip_ssl_validation": "true", "cnsi_client_id": mockClientId, "cnsi_client_secret": mockClientSecret, - "overwrite_endpoints": strconv.FormatBool(overwriteEndpoints), + "create_user_endpoint": strconv.FormatBool(createUserEndpoint), }) res := httptest.NewRecorder() diff --git a/src/jetstream/repository/interfaces/portal_proxy.go b/src/jetstream/repository/interfaces/portal_proxy.go index c31e21bd26..8b84ced2fc 100644 --- a/src/jetstream/repository/interfaces/portal_proxy.go +++ b/src/jetstream/repository/interfaces/portal_proxy.go @@ -14,7 +14,7 @@ type PortalProxy interface { GetHttpClient(skipSSLValidation bool) http.Client GetHttpClientForRequest(req *http.Request, skipSSLValidation bool) http.Client RegisterEndpoint(c echo.Context, fetchInfo InfoFunc) error - DoRegisterEndpoint(cnsiName string, apiEndpoint string, skipSSLValidation bool, clientId string, clientSecret string, userId string, ssoAllowed bool, subType string, overwriteEndpoints bool, fetchInfo InfoFunc) (CNSIRecord, error) + DoRegisterEndpoint(cnsiName string, apiEndpoint string, skipSSLValidation bool, clientId string, clientSecret string, userId string, ssoAllowed bool, subType string, createUserEndpoint bool, fetchInfo InfoFunc) (CNSIRecord, error) GetEndpointTypeSpec(typeName string) (EndpointPlugin, error) // Auth diff --git a/src/jetstream/repository/interfaces/structs.go b/src/jetstream/repository/interfaces/structs.go index 8ecf6fafd2..fccdc53ebc 100644 --- a/src/jetstream/repository/interfaces/structs.go +++ b/src/jetstream/repository/interfaces/structs.go @@ -433,7 +433,7 @@ type RegisterEndpointParams struct { CNSIClientID string `json:"cnsi_client_id" form:"cnsi_client_id" query:"cnsi_client_id"` CNSIClientSecret string `json:"cnsi_client_secret" form:"cnsi_client_secret" query:"cnsi_client_secret"` SubType string `json:"sub_type" form:"sub_type" query:"sub_type"` - OverwriteEndpoints string `json:"overwrite_endpoints" form:"overwrite_endpoints" query:"overwrite_endpoints"` + CreateUserEndpoint string `json:"create_user_endpoint" form:"create_user_endpoint" query:"create_user_endpoint"` } type UpdateEndpointParams struct { From 1cad542505f85c4190426139de157349a0e4bc7f Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 1 Apr 2021 15:18:54 +0200 Subject: [PATCH 093/101] Admins can create personal endpoints (#4876) Signed-off-by: Thomas Quandt --- .../create-endpoint-cf-step-1.component.html | 4 +-- .../create-endpoint/create-endpoint-helper.ts | 29 ++++++++++--------- .../endpoint/base-endpoints-data-source.ts | 3 +- .../git-registration.component.html | 4 +-- .../store/src/types/endpoint.types.ts | 1 + src/jetstream/cnsi.go | 24 ++------------- src/jetstream/info.go | 10 +++++-- .../repository/interfaces/structs.go | 5 ++-- 8 files changed, 36 insertions(+), 44 deletions(-) diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html index 07c3a7c620..53bff3bfbb 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html @@ -2,14 +2,14 @@

{{endpoint.definition.label}} Information

+ [appUnique]="registerForm.value.createUserEndpointField ? (existingPersonalEndpoints | async)?.names : (customEndpoints | async)?.names"> Name is required Name is not unique + [appUnique]="registerForm.value.createUserEndpointField ? (existingPersonalEndpoints | async)?.urls : (customEndpoints | async)?.urls"> URL is required Invalid API URL URL is not unique diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper.ts index d198b839c6..789a60e721 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper.ts @@ -16,25 +16,28 @@ export class CreateEndpointHelperComponent { userEndpointsAndIsAdmin: Observable; customEndpoints: EndpointObservable; + existingSystemEndpoints: EndpointObservable; + existingPersonalEndpoints: EndpointObservable; + existingEndpoints: EndpointObservable; constructor( public sessionService: SessionService, public currentUserPermissionsService: CurrentUserPermissionsService ) { const currentPage$ = stratosEntityCatalog.endpoint.store.getAll.getPaginationMonitor().currentPage$; - const existingAdminEndpoints = currentPage$.pipe( + this.existingSystemEndpoints = currentPage$.pipe( map(endpoints => ({ - names: endpoints.filter(ep => ep.creator.admin).map(ep => ep.name), - urls: endpoints.filter(ep => ep.creator.admin).map(ep => getFullEndpointApiUrl(ep)), + names: endpoints.filter(ep => ep.creator.system).map(ep => ep.name), + urls: endpoints.filter(ep => ep.creator.system).map(ep => getFullEndpointApiUrl(ep)), })) ); - const existingUserEndpoints = currentPage$.pipe( + this.existingPersonalEndpoints = currentPage$.pipe( map(endpoints => ({ - names: endpoints.filter(ep => !ep.creator.admin).map(ep => ep.name), - urls: endpoints.filter(ep => !ep.creator.admin).map(ep => getFullEndpointApiUrl(ep)), + names: endpoints.filter(ep => !ep.creator.system).map(ep => ep.name), + urls: endpoints.filter(ep => !ep.creator.system).map(ep => getFullEndpointApiUrl(ep)), })) ); - const existingEndpoints = currentPage$.pipe( + this.existingEndpoints = currentPage$.pipe( map(endpoints => ({ names: endpoints.map(ep => ep.name), urls: endpoints.map(ep => getFullEndpointApiUrl(ep)), @@ -54,16 +57,16 @@ export class CreateEndpointHelperComponent { this.customEndpoints = combineLatest([ userEndpointsNotDisabled, isAdmin, - existingEndpoints, - existingAdminEndpoints, - existingUserEndpoints + this.existingEndpoints, + this.existingSystemEndpoints, + this.existingPersonalEndpoints ]).pipe( - map(([userEndpointsEnabled, admin, endpoints, adminEndpoints, userEndpoints]) => { + map(([userEndpointsEnabled, admin, endpoints, systemEndpoints, personalEndpoints]) => { if (userEndpointsEnabled){ if (admin){ - return adminEndpoints; + return systemEndpoints; }else{ - return userEndpoints; + return personalEndpoints; } } return endpoints; diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts index bd1a2a8b00..cb329f65d0 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/base-endpoints-data-source.ts @@ -125,7 +125,8 @@ export class BaseEndpointsDataSource extends ListDataSource { sso_allowed: false, creator: { name: '', - admin: false + admin: false, + system: false } }), paginationKey: action.paginationKey, diff --git a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.html b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.html index 21572423a8..67198988be 100644 --- a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.html +++ b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.html @@ -19,14 +19,14 @@

Select the type of {{gitTypes[epSubType].label}} to register

+ [appUnique]="registerForm.value.createUserEndpointField ? (existingPersonalEndpoints | async)?.names : (customEndpoints | async)?.names"> Name is required Name is not unique + [appUnique]="registerForm.value.createUserEndpointField ? (existingPersonalEndpoints | async)?.urls : (customEndpoints | async)?.urls"> URL is required Invalid API URL URL is not unique diff --git a/src/frontend/packages/store/src/types/endpoint.types.ts b/src/frontend/packages/store/src/types/endpoint.types.ts index 68a77ed2a3..feb3c51906 100644 --- a/src/frontend/packages/store/src/types/endpoint.types.ts +++ b/src/frontend/packages/store/src/types/endpoint.types.ts @@ -68,6 +68,7 @@ export interface EndpointUser { export interface CreatorInfo { name: string; admin: boolean; + system: boolean; } export interface EndpointState { diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index ac6159ca67..de595ab422 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -150,7 +150,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk // check if we've already got this APIEndpoint in this DB for _, duplicate := range duplicateEndpoints { // cant create same system endpoint - if len(duplicate.Creator) == 0 && isAdmin { + if len(duplicate.Creator) == 0 && isAdmin && !createUserEndpoint { return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( http.StatusBadRequest, "Can not register same system endpoint multiple times", @@ -159,7 +159,6 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk } // cant create same user endpoint - // can create same user endpoint if overwriteEndpoint true if duplicate.Creator == userId { return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( http.StatusBadRequest, @@ -168,28 +167,11 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk ) } } - - /* - if isAdmin && overwriteEndpoints { - for _, duplicate := range duplicateEndpoints { - log.Infof("An administrator is registering an endpoint with the same API URL ('%+v') as an endpoint administrator's. The existing duplicate endpoint ('%+v') will be removed", apiEndpoint, duplicate.GUID) - err := p.doUnregisterCluster(duplicate.GUID) - if err != nil { - return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( - http.StatusInternalServerError, - "Failed to unregister cluster", - "Failed to unregister cluster: %v", - err) - } - } - } - */ - } h := sha1.New() // see why its generated this way in Issue #4753 / #3031 - if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled && !isAdmin { + if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled && (!isAdmin || createUserEndpoint) { // Make the new guid unique per api url AND user id h.Write([]byte(apiEndpointURL.String() + userId)) } else { @@ -222,7 +204,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk newCNSI.SubType = subType // admins currently can't create user endpoints - if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled && !isAdmin { + if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled && (!isAdmin || createUserEndpoint) { newCNSI.Creator = userId } diff --git a/src/jetstream/info.go b/src/jetstream/info.go index ea4a57b719..e449f2b67f 100644 --- a/src/jetstream/info.go +++ b/src/jetstream/info.go @@ -102,18 +102,22 @@ func (p *portalProxy) getInfo(c echo.Context) (*interfaces.Info, error) { // set the creator preemptively as admin, if no id is found endpoint.Creator = &interfaces.CreatorInfo{ - Name: "admin", - Admin: true, + Name: "system", + Admin: false, + System: true, } // assume it's a user when len != 0 if len(cnsi.Creator) != 0 { - endpoint.Creator.Admin = false + endpoint.Creator.System = false u, err := p.StratosAuthService.GetUser(cnsi.Creator) + // add an anonymous user if no user is found if err != nil { endpoint.Creator.Name = "user" + endpoint.Creator.Admin = false } else { endpoint.Creator.Name = u.Name + endpoint.Creator.Admin = u.Admin } } diff --git a/src/jetstream/repository/interfaces/structs.go b/src/jetstream/repository/interfaces/structs.go index fccdc53ebc..66d38e25f1 100644 --- a/src/jetstream/repository/interfaces/structs.go +++ b/src/jetstream/repository/interfaces/structs.go @@ -195,8 +195,9 @@ type ConnectedUser struct { // CreatorInfo - additional information about the user who created an endpoint type CreatorInfo struct { - Name string `json:"name"` - Admin bool `json:"admin"` + Name string `json:"name"` + Admin bool `json:"admin"` + System bool `json:"system"` } type JWTUserTokenInfo struct { From 04aa4d58b1bcebf5cff20b45713167ba77cb6711 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 1 Apr 2021 18:01:37 +0200 Subject: [PATCH 094/101] Automatic disconnect by same url endpoint connect (#4876) Signed-off-by: Thomas Quandt --- .../endpoint/endpoint-list.helpers.ts | 8 +++--- src/jetstream/authcnsi.go | 26 ++++++++++++++----- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts index 1954136782..f65ac5ef15 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts @@ -16,6 +16,7 @@ import { ConnectEndpointDialogComponent, } from '../../../../../features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component'; import { SessionService } from '../../../../../shared/services/session.service'; +import { UserProfileService } from '../../../../../core/user-profile.service'; import { SnackBarService } from '../../../../services/snackbar.service'; import { ConfirmationDialogConfig } from '../../../confirmation-dialog.config'; import { ConfirmationDialogService } from '../../../confirmation-dialog.service'; @@ -68,6 +69,7 @@ export class EndpointListHelper { private confirmDialog: ConfirmationDialogService, private snackBarService: SnackBarService, private sessionService: SessionService, + private userProfileService: UserProfileService ) { } endpointActions(includeSeparators = false): IListAction[] { @@ -137,11 +139,11 @@ export class EndpointListHelper { createVisible: (row$: Observable) => { return combineLatest([ this.sessionService.userEndpointsNotDisabled(), - this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT), + this.userProfileService.userProfile$, row$ ]).pipe( - map(([userEndpointsEnabled, isAdmin, row]) => { - if (userEndpointsEnabled && !row.creator.admin && isAdmin) { + map(([userEndpointsEnabled, profile, row]) => { + if (userEndpointsEnabled && !row.creator.system && profile.userName !== row.creator.name) { // Disable connect for admins if the endpoint was not created by them. Otherwise this could result in an admin connecting to // multiple user endpoints that all have the same url. return false; diff --git a/src/jetstream/authcnsi.go b/src/jetstream/authcnsi.go index 2b9db5de5f..84f0af9bbe 100644 --- a/src/jetstream/authcnsi.go +++ b/src/jetstream/authcnsi.go @@ -151,18 +151,30 @@ func (p *portalProxy) DoLoginToCNSI(c echo.Context, cnsiGUID string, systemShare } // admins are note allowed to connect to user created endpoints - if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled && len(cnsiRecord.Creator) != 0 { - user, err := p.StratosAuthService.GetUser(userID) + if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled { + + // search for system or personal endpoints and check if they are connected + // automatically disconnect other endpoint if already connected to same url + cnsiList, err := p.listCNSIByAPIEndpoint(cnsiRecord.APIEndpoint.String()) if err != nil { - return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - could not check user") + return nil, echo.NewHTTPError( + http.StatusBadRequest, + "Failed to retrieve list of CNSIs", + "Failed to retrieve list of CNSIs: %v", err, + ) } - if user.Admin { - return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - admins are not allowed to connect to user created endpoints") + for _, cnsi := range cnsiList { + if cnsi.Creator == userID || len(cnsi.Creator) == 0 { + _, ok := p.GetCNSITokenRecord(cnsi.GUID, userID) + if ok { + p.ClearCNSIToken(*cnsi, userID) + } + } } - if cnsiRecord.Creator != userID { - return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - non-admins are not allowed to connect to endpoints created by other non-admins") + if len(cnsiRecord.Creator) != 0 && cnsiRecord.Creator != userID { + return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - users are not allowed to connect to personal endpoints created by other users") } } From 46d16dfc1c5f804d8706a2f1c7883cc652689e8d Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Tue, 6 Apr 2021 15:13:05 +0200 Subject: [PATCH 095/101] Adjust unit tests (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/auth_test.go | 69 ++++++++++++++++------ src/jetstream/authcnsi.go | 10 ++-- src/jetstream/cnsi.go | 2 +- src/jetstream/cnsi_test.go | 98 +++++++++++++++++++------------ src/jetstream/mock_server_test.go | 15 ++--- 5 files changed, 123 insertions(+), 71 deletions(-) diff --git a/src/jetstream/auth_test.go b/src/jetstream/auth_test.go index 5eeda668f1..9df9c09794 100644 --- a/src/jetstream/auth_test.go +++ b/src/jetstream/auth_test.go @@ -640,15 +640,19 @@ func TestLoginToCNSIWithUserEndpointsEnabled(t *testing.T) { }) _, ctxConnectToUser2 := setupEchoContext(res, req) + adminAndUserEndpointRows := sqlmock.NewRows(rowFieldsForCNSI).AddRow(adminEndpointArgs...).AddRow(userEndpoint1Args...) + Convey("As admin", func() { - Convey("Connect to admin endpoint", func() { + Convey("Connect to system endpoint", func() { if errSession := pp.setSessionValues(ctxConnectToAdmin, mockAdmin.SessionValues); errSession != nil { t.Error(errors.New("unable to mock/stub user in session object")) } mock.ExpectQuery(selectFromCNSIs).WillReturnRows(adminEndpointRows) + mock.ExpectQuery(selectAnyFromCNSIs).WillReturnRows(adminEndpointRows) + mock.ExpectQuery(selectAnyFromTokens). WithArgs(adminEndpointArgs[0], mockAdmin.ConnectedUser.GUID). WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow("0")) @@ -674,22 +678,16 @@ func TestLoginToCNSIWithUserEndpointsEnabled(t *testing.T) { mock.ExpectQuery(selectFromCNSIs).WillReturnRows(userEndpoint1Rows) - mockStratosAuth. - EXPECT(). - GetUser(gomock.Eq(mockAdmin.ConnectedUser.GUID)). - Return(mockAdmin.ConnectedUser, nil) - err := pp.loginToCNSI(ctxConnectToUser1) dberr := mock.ExpectationsWereMet() Convey("should fail", func() { - So(err, ShouldResemble, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - admins are not allowed to connect to user created endpoints")) + So(err, ShouldResemble, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - users are not allowed to connect to personal endpoints created by other users")) }) Convey("there should be no db error", func() { So(dberr, ShouldBeNil) }) - }) }) Convey("As user", func() { @@ -698,13 +696,49 @@ func TestLoginToCNSIWithUserEndpointsEnabled(t *testing.T) { t.Error(errors.New("unable to mock/stub user in session object")) } - mockStratosAuth. - EXPECT(). - GetUser(gomock.Eq(mockEndpointAdmin1.ConnectedUser.GUID)). - Return(mockEndpointAdmin1.ConnectedUser, nil) + mock.ExpectQuery(selectFromCNSIs).WillReturnRows(userEndpoint1Rows) + + mock.ExpectQuery(selectAnyFromCNSIs).WillReturnRows(userEndpoint1Rows) + + mock.ExpectQuery(selectAnyFromTokens). + WithArgs(userEndpoint1Args[0], mockEndpointAdmin1.ConnectedUser.GUID). + WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow("0")) + + mock.ExpectExec(insertIntoTokens). + WillReturnResult(sqlmock.NewResult(1, 1)) + + err := pp.loginToCNSI(ctxConnectToUser1) + dberr := mock.ExpectationsWereMet() + + Convey("there should be no error", func() { + So(err, ShouldBeNil) + }) + + Convey("there should be no db error", func() { + So(dberr, ShouldBeNil) + }) + }) + Convey("Connect to own endpoint while already connected to same url with system endpoint", func() { + if errSession := pp.setSessionValues(ctxConnectToUser1, mockEndpointAdmin1.SessionValues); errSession != nil { + t.Error(errors.New("unable to mock/stub user in session object")) + } mock.ExpectQuery(selectFromCNSIs).WillReturnRows(userEndpoint1Rows) + // args is the api url + mock.ExpectQuery(selectAnyFromCNSIs).WithArgs(userEndpoint1Args[3]).WillReturnRows(adminAndUserEndpointRows) + + // connected system endpoint found + mock.ExpectQuery(selectAnyFromTokens). + WithArgs(adminEndpointArgs[0], mockEndpointAdmin1.ConnectedUser.GUID, mockAdminGUID). + WillReturnRows(sqlmock.NewRows([]string{"token_guid", "auth_token", "refresh_token", "token_expiry", "disconnected", "auth_type", "meta_data", "user_guid", "linked_token"}). + AddRow("", mockUAAToken, mockUAAToken, time.Now().Add(-time.Hour).Unix(), false, "", "", "", nil)) + + // remove other connection, since it has the same api url + mock.ExpectExec(deleteTokens). + WithArgs(adminEndpointArgs[0], mockEndpointAdmin1.ConnectedUser.GUID). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectQuery(selectAnyFromTokens). WithArgs(userEndpoint1Args[0], mockEndpointAdmin1.ConnectedUser.GUID). WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow("0")) @@ -723,13 +757,15 @@ func TestLoginToCNSIWithUserEndpointsEnabled(t *testing.T) { So(dberr, ShouldBeNil) }) }) - Convey("Connect to admin endpoint", func() { + Convey("Connect to system endpoint", func() { if errSession := pp.setSessionValues(ctxConnectToAdmin, mockEndpointAdmin1.SessionValues); errSession != nil { t.Error(errors.New("unable to mock/stub user in session object")) } mock.ExpectQuery(selectFromCNSIs).WillReturnRows(adminEndpointRows) + mock.ExpectQuery(selectAnyFromCNSIs).WillReturnRows(adminEndpointRows) + mock.ExpectQuery(selectAnyFromTokens). WithArgs(adminEndpointArgs[0], mockEndpointAdmin1.ConnectedUser.GUID). WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow("0")) @@ -755,16 +791,11 @@ func TestLoginToCNSIWithUserEndpointsEnabled(t *testing.T) { mock.ExpectQuery(selectFromCNSIs).WillReturnRows(userEndpoint2Rows) - mockStratosAuth. - EXPECT(). - GetUser(gomock.Eq(mockEndpointAdmin1.ConnectedUser.GUID)). - Return(mockEndpointAdmin1.ConnectedUser, nil) - err := pp.loginToCNSI(ctxConnectToUser2) dberr := mock.ExpectationsWereMet() Convey("should fail", func() { - So(err, ShouldResemble, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - non-admins are not allowed to connect to endpoints created by other non-admins")) + So(err, ShouldResemble, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - users are not allowed to connect to personal endpoints created by other users")) }) Convey("there should be no db error", func() { diff --git a/src/jetstream/authcnsi.go b/src/jetstream/authcnsi.go index 84f0af9bbe..ba446ef2a4 100644 --- a/src/jetstream/authcnsi.go +++ b/src/jetstream/authcnsi.go @@ -153,6 +153,10 @@ func (p *portalProxy) DoLoginToCNSI(c echo.Context, cnsiGUID string, systemShare // admins are note allowed to connect to user created endpoints if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled { + if len(cnsiRecord.Creator) != 0 && cnsiRecord.Creator != userID { + return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - users are not allowed to connect to personal endpoints created by other users") + } + // search for system or personal endpoints and check if they are connected // automatically disconnect other endpoint if already connected to same url cnsiList, err := p.listCNSIByAPIEndpoint(cnsiRecord.APIEndpoint.String()) @@ -165,17 +169,13 @@ func (p *portalProxy) DoLoginToCNSI(c echo.Context, cnsiGUID string, systemShare } for _, cnsi := range cnsiList { - if cnsi.Creator == userID || len(cnsi.Creator) == 0 { + if (cnsi.Creator == userID || len(cnsi.Creator) == 0) && cnsi.GUID != cnsiGUID { _, ok := p.GetCNSITokenRecord(cnsi.GUID, userID) if ok { p.ClearCNSIToken(*cnsi, userID) } } } - - if len(cnsiRecord.Creator) != 0 && cnsiRecord.Creator != userID { - return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect - users are not allowed to connect to personal endpoints created by other users") - } } // Register as a system endpoint? diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index de595ab422..2cd160eb45 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -283,7 +283,7 @@ func (p *portalProxy) buildCNSIList(c echo.Context) ([]*interfaces.CNSIRecord, e } if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.AdminOnly { - // remove existing system endpoint if user endpoint already exists and sessionuser not admin + // if endpoint with same url exists as system and user endpoint, hide the system endpoint unfilteredList, err := p.ListAdminEndpoints(userID.(string)) if err != nil { return unfilteredList, err diff --git a/src/jetstream/cnsi_test.go b/src/jetstream/cnsi_test.go index 635a8f08be..393f8ba93d 100644 --- a/src/jetstream/cnsi_test.go +++ b/src/jetstream/cnsi_test.go @@ -307,9 +307,9 @@ func TestRegisterWithUserEndpointsEnabled(t *testing.T) { pp.GetConfig().UserEndpointsEnabled = config.UserEndpointsConfigEnum.Enabled Convey("as admin", func() { - Convey("with overwrite disabled", func() { + Convey("with createUserEndpoint disabled", func() { // setup - adminEndpoint := setupMockEndpointRegisterRequest(t, mockAdmin.ConnectedUser, mockV2Info[0], "CF Cluster 1", false) + adminEndpoint := setupMockEndpointRegisterRequest(t, mockAdmin.ConnectedUser, mockV2Info[0], "CF Cluster 1", false, true) if errSession := pp.setSessionValues(adminEndpoint.EchoContext, mockAdmin.SessionValues); errSession != nil { t.Error(errors.New("unable to mock/stub user in session object")) @@ -341,9 +341,9 @@ func TestRegisterWithUserEndpointsEnabled(t *testing.T) { So(dberr, ShouldBeNil) }) }) - Convey("overwrite existing user endpoints", func() { + Convey("create system endpoint over existing user endpoints", func() { // setup - userEndpoint := setupMockEndpointRegisterRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1 User", false) + userEndpoint := setupMockEndpointRegisterRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1 User", false, false) // mock executions mockStratosAuth. @@ -355,33 +355,24 @@ func TestRegisterWithUserEndpointsEnabled(t *testing.T) { rows := sqlmock.NewRows(rowFieldsForCNSI).AddRow(userEndpoint.QueryArgs...) mock.ExpectQuery(selectAnyFromCNSIs).WithArgs(mockV2Info[0].URL).WillReturnRows(rows) + // save cnsi + mock.ExpectExec(insertIntoCNSIs). + WithArgs(adminEndpoint.InsertArgs...). + WillReturnResult(sqlmock.NewResult(1, 1)) + // test err := pp.RegisterEndpoint(adminEndpoint.EchoContext, getCFPlugin(pp, "cf").Info) dberr := mock.ExpectationsWereMet() Convey("there should be no error", func() { - So(err, ShouldResemble, interfaces.NewHTTPShadowError( - http.StatusBadRequest, - "Can not register same endpoint multiple times", - "Can not register same endpoint multiple times", - )) + So(err, ShouldBeNil) }) Convey("there should be no db error", func() { So(dberr, ShouldBeNil) }) }) - }) - Convey("with overwrite enabled", func() { - - // setup - adminEndpoint := setupMockEndpointRegisterRequest(t, mockAdmin.ConnectedUser, mockV2Info[0], "CF Cluster 1", true) - - if errSession := pp.setSessionValues(adminEndpoint.EchoContext, mockAdmin.SessionValues); errSession != nil { - t.Error(errors.New("unable to mock/stub user in session object")) - } - - Convey("overwrite existing admin endpoints", func() { + Convey("create system endpoint over existing system endpoints", func() { // mock executions mockStratosAuth. EXPECT(). @@ -399,8 +390,8 @@ func TestRegisterWithUserEndpointsEnabled(t *testing.T) { Convey("should fail ", func() { So(err, ShouldResemble, interfaces.NewHTTPShadowError( http.StatusBadRequest, - "Can not register same admin endpoint multiple times", - "Can not register same admin endpoint multiple times", + "Can not register same system endpoint multiple times", + "Can not register same system endpoint multiple times", )) }) @@ -408,27 +399,29 @@ func TestRegisterWithUserEndpointsEnabled(t *testing.T) { So(dberr, ShouldBeNil) }) }) - Convey("overwrite existing user endpoints", func() { + }) + Convey("with createUserEndpoint enabled", func() { - // setup - userEndpoint := setupMockEndpointRegisterRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1 User", false) + // setup + adminEndpoint := setupMockEndpointRegisterRequest(t, mockAdmin.ConnectedUser, mockV2Info[0], "CF Cluster 1", true, false) + systemEndpoint := setupMockEndpointRegisterRequest(t, mockAdmin.ConnectedUser, mockV2Info[0], "CF Cluster 1", true, true) + + if errSession := pp.setSessionValues(adminEndpoint.EchoContext, mockAdmin.SessionValues); errSession != nil { + t.Error(errors.New("unable to mock/stub user in session object")) + } + Convey("register personal endpoint over system endpoint", func() { // mock executions mockStratosAuth. EXPECT(). GetUser(gomock.Eq(mockAdmin.ConnectedUser.GUID)). Return(mockAdmin.ConnectedUser, nil) - // return a user endpoint with same apiurl - rows := sqlmock.NewRows(rowFieldsForCNSI).AddRow(userEndpoint.QueryArgs...) + // return a admin endpoint with same apiurl + rows := sqlmock.NewRows(rowFieldsForCNSI).AddRow(systemEndpoint.QueryArgs...) mock.ExpectQuery(selectAnyFromCNSIs).WithArgs(mockV2Info[0].URL).WillReturnRows(rows) - // user endpoints should be deleted - mock.ExpectExec(deleteFromCNSIs). - WithArgs(userEndpoint.QueryArgs[0]). - WillReturnResult(sqlmock.NewResult(1, 1)) - - // a new admin endpoint with same url will be registered + // save cnsi mock.ExpectExec(insertIntoCNSIs). WithArgs(adminEndpoint.InsertArgs...). WillReturnResult(sqlmock.NewResult(1, 1)) @@ -441,6 +434,33 @@ func TestRegisterWithUserEndpointsEnabled(t *testing.T) { So(err, ShouldBeNil) }) + Convey("there should be no db error", func() { + So(dberr, ShouldBeNil) + }) + }) + Convey("register personal endpoint twice", func() { + // mock executions + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockAdmin.ConnectedUser.GUID)). + Return(mockAdmin.ConnectedUser, nil) + + // return a user endpoint with same apiurl + rows := sqlmock.NewRows(rowFieldsForCNSI).AddRow(adminEndpoint.QueryArgs...) + mock.ExpectQuery(selectAnyFromCNSIs).WithArgs(mockV2Info[0].URL).WillReturnRows(rows) + + // test + err := pp.RegisterEndpoint(adminEndpoint.EchoContext, getCFPlugin(pp, "cf").Info) + dberr := mock.ExpectationsWereMet() + + Convey("there should be no error", func() { + So(err, ShouldResemble, interfaces.NewHTTPShadowError( + http.StatusBadRequest, + "Can not register same endpoint multiple times", + "Can not register same endpoint multiple times", + )) + }) + Convey("there should be no db error", func() { So(dberr, ShouldBeNil) }) @@ -449,9 +469,9 @@ func TestRegisterWithUserEndpointsEnabled(t *testing.T) { }) Convey("as user", func() { - Convey("with overwrite disabled", func() { + Convey("with createUserEndpoint disabled", func() { // setup - userEndpoint := setupMockEndpointRegisterRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1", false) + userEndpoint := setupMockEndpointRegisterRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1", false, false) if errSession := pp.setSessionValues(userEndpoint.EchoContext, mockUser1.SessionValues); errSession != nil { t.Error(errors.New("unable to mock/stub user in session object")) @@ -483,7 +503,7 @@ func TestRegisterWithUserEndpointsEnabled(t *testing.T) { }) }) Convey("register existing endpoint from different user", func() { - userEndpoint2 := setupMockEndpointRegisterRequest(t, mockUser2.ConnectedUser, mockV2Info[0], "CF Cluster 2", false) + userEndpoint2 := setupMockEndpointRegisterRequest(t, mockUser2.ConnectedUser, mockV2Info[0], "CF Cluster 2", false, false) // mock executions mockStratosAuth. @@ -535,13 +555,13 @@ func TestRegisterWithUserEndpointsEnabled(t *testing.T) { }) }) }) - Convey("with overwrite enabled", func() { - userEndpoint := setupMockEndpointRegisterRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1", false) + Convey("with createUserEndpoint enabled", func() { + userEndpoint := setupMockEndpointRegisterRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1", true, false) if errSession := pp.setSessionValues(userEndpoint.EchoContext, mockUser1.SessionValues); errSession != nil { t.Error(errors.New("unable to mock/stub user in session object")) } - Convey("overwrite existing endpoints from same user, with overwrite enabled", func() { + Convey("register existing endpoint from same user", func() { // mock executions mockStratosAuth. EXPECT(). diff --git a/src/jetstream/mock_server_test.go b/src/jetstream/mock_server_test.go index ec4c9e2cf6..8e96ff2724 100644 --- a/src/jetstream/mock_server_test.go +++ b/src/jetstream/mock_server_test.go @@ -272,15 +272,15 @@ func setupMockUser(guid string, admin bool, scopes []string) MockUser { } // mockV2Info needs to be closed -func setupMockEndpointRegisterRequest(t *testing.T, user *interfaces.ConnectedUser, mockV2Info *httptest.Server, endpointName string, createUserEndpoint bool) MockEndpointRequest { +func setupMockEndpointRegisterRequest(t *testing.T, user *interfaces.ConnectedUser, mockV2Info *httptest.Server, endpointName string, createUserEndpoint bool, generateAdminGUID bool) MockEndpointRequest { // create a request for each endpoint req := setupMockReq("POST", "", map[string]string{ - "cnsi_name": endpointName, - "api_endpoint": mockV2Info.URL, - "skip_ssl_validation": "true", - "cnsi_client_id": mockClientId, - "cnsi_client_secret": mockClientSecret, + "cnsi_name": endpointName, + "api_endpoint": mockV2Info.URL, + "skip_ssl_validation": "true", + "cnsi_client_id": mockClientId, + "cnsi_client_secret": mockClientSecret, "create_user_endpoint": strconv.FormatBool(createUserEndpoint), }) @@ -290,7 +290,7 @@ func setupMockEndpointRegisterRequest(t *testing.T, user *interfaces.ConnectedUs uaaUserGUID := "" h := sha1.New() - if user.Admin { + if generateAdminGUID { h.Write([]byte(mockV2Info.URL)) } else { h.Write([]byte(mockV2Info.URL + user.GUID)) @@ -377,6 +377,7 @@ const ( selectAnyFromTokens = `SELECT (.+) FROM tokens WHERE (.+)` insertIntoTokens = `INSERT INTO tokens` updateTokens = `UPDATE tokens` + deleteTokens = `DELETE FROM tokens WHERE (.+)` selectFromCNSIs = `SELECT (.+) FROM cnsis` selectAnyFromCNSIs = `SELECT (.+) FROM cnsis WHERE (.+)` selectCreatorFromCNSIs = `SELECT (.+) FROM cnsis WHERE creator=(.+)` From e871659008860901c844ae319931147924857fe7 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Tue, 6 Apr 2021 16:08:22 +0200 Subject: [PATCH 096/101] Adjust front-end tests (#4876) Signed-off-by: Thomas Quandt --- .../current-user-permissions-and-cfchecker.service.spec.ts | 6 ++++-- .../permissions/current-user-permissions.service.spec.ts | 6 ++++-- .../packages/store/testing/src/store-test-helper.ts | 6 ++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/frontend/packages/cloud-foundry/src/shared/services/current-user-permissions-and-cfchecker.service.spec.ts b/src/frontend/packages/cloud-foundry/src/shared/services/current-user-permissions-and-cfchecker.service.spec.ts index e94acc141f..c41db83388 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/services/current-user-permissions-and-cfchecker.service.spec.ts +++ b/src/frontend/packages/cloud-foundry/src/shared/services/current-user-permissions-and-cfchecker.service.spec.ts @@ -66,7 +66,8 @@ describe('CurrentUserPermissionsService with CF checker', () => { }, creator: { name: 'admin', - admin: true + admin: true, + system: false }, metricsAvailable: false, connectionStatus: 'connected', @@ -108,7 +109,8 @@ describe('CurrentUserPermissionsService with CF checker', () => { }, creator: { name: 'admin', - admin: true + admin: true, + system: false }, metricsAvailable: false, connectionStatus: 'connected', diff --git a/src/frontend/packages/core/src/core/permissions/current-user-permissions.service.spec.ts b/src/frontend/packages/core/src/core/permissions/current-user-permissions.service.spec.ts index 71f2e804c9..d6c2ee25f4 100644 --- a/src/frontend/packages/core/src/core/permissions/current-user-permissions.service.spec.ts +++ b/src/frontend/packages/core/src/core/permissions/current-user-permissions.service.spec.ts @@ -52,7 +52,8 @@ describe('CurrentUserPermissionsService', () => { }, creator: { name: 'admin', - admin: true + admin: true, + system: false }, metricsAvailable: false, connectionStatus: 'connected', @@ -89,7 +90,8 @@ describe('CurrentUserPermissionsService', () => { }, creator: { name: 'admin', - admin: true + admin: true, + system: false }, metricsAvailable: false, connectionStatus: 'connected', diff --git a/src/frontend/packages/store/testing/src/store-test-helper.ts b/src/frontend/packages/store/testing/src/store-test-helper.ts index 919142e94c..de91d34d68 100644 --- a/src/frontend/packages/store/testing/src/store-test-helper.ts +++ b/src/frontend/packages/store/testing/src/store-test-helper.ts @@ -49,7 +49,8 @@ export const testSCFEndpoint: EndpointModel = { metricsAvailable: false, creator: { name: 'admin', - admin: true + admin: true, + system: false } }; @@ -305,7 +306,8 @@ function getDefaultInitialTestStoreState(): AppState { }, creator: { name: 'admin', - admin: true + admin: true, + system: false }, connectionStatus: 'connected', system_shared_token: false, From 5a500e2e3ce3acc580ae1a94b0035f1c12dc5f0e Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Tue, 13 Apr 2021 11:17:52 +0200 Subject: [PATCH 097/101] Change createUserEndpoint checkbox & fix validation bug (#4876) Signed-off-by: Thomas Quandt --- .../create-endpoint-cf-step-1.component.html | 10 +++++----- .../create-endpoint-cf-step-1.component.ts | 15 ++++++++------- .../create-endpoint/create-endpoint-helper.ts | 15 ++++++++++----- .../git-registration.component.html | 10 +++++----- .../git-registration.component.ts | 14 ++++++++------ .../store/src/actions/endpoint.actions.ts | 2 +- .../store/src/effects/endpoint.effects.ts | 2 +- .../store/src/stratos-action-builders.ts | 6 +++--- src/jetstream/cnsi.go | 16 ++++++++-------- src/jetstream/cnsi_test.go | 18 +++++++++--------- src/jetstream/mock_server_test.go | 14 +++++++------- .../repository/interfaces/portal_proxy.go | 2 +- src/jetstream/repository/interfaces/structs.go | 18 +++++++++--------- 13 files changed, 75 insertions(+), 67 deletions(-) diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html index 53bff3bfbb..0beca26849 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html @@ -2,21 +2,21 @@

{{endpoint.definition.label}} Information

+ [appUnique]="registerForm.value.createSystemEndpointField ? (customEndpoints | async)?.names : (existingPersonalEndpoints | async)?.names"> Name is required Name is not unique + [appUnique]="registerForm.value.createSystemEndpointField ? (customEndpoints | async)?.urls : (existingPersonalEndpoints | async)?.urls"> URL is required Invalid API URL URL is not unique - Create a user endpoint (only visible to you and other admins) + Create a system endpoint (visible to all users) Skip SSL validation for the diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts index 5bd40be54b..013462f7af 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.ts @@ -2,7 +2,7 @@ import { AfterContentInit, Component, Input } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; -import { filter, map, pairwise } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, pairwise } from 'rxjs/operators'; import { entityCatalog } from '../../../../../../store/src/entity-catalog/entity-catalog'; import { @@ -14,12 +14,12 @@ import { getIdFromRoute } from '../../../../core/utils.service'; import { IStepperStep, StepOnNextFunction } from '../../../../shared/components/stepper/step/step.component'; import { SessionService } from '../../../../shared/services/session.service'; import { CurrentUserPermissionsService } from '../../../../core/permissions/current-user-permissions.service'; +import { UserProfileService } from '../../../../core/user-profile.service'; import { SnackBarService } from '../../../../shared/services/snackbar.service'; import { ConnectEndpointConfig } from '../../connect.service'; import { getSSOClientRedirectURI } from '../../endpoint-helpers'; import { CreateEndpointHelperComponent } from '../create-endpoint-helper'; - @Component({ selector: 'app-create-endpoint-cf-step-1', templateUrl: './create-endpoint-cf-step-1.component.html', @@ -63,9 +63,10 @@ export class CreateEndpointCfStep1Component extends CreateEndpointHelperComponen activatedRoute: ActivatedRoute, private snackBarService: SnackBarService, sessionService: SessionService, - currentUserPermissionsService: CurrentUserPermissionsService + currentUserPermissionsService: CurrentUserPermissionsService, + userProfileService: UserProfileService ) { - super(sessionService, currentUserPermissionsService); + super(sessionService, currentUserPermissionsService, userProfileService); this.registerForm = this.fb.group({ nameField: ['', [Validators.required]], @@ -75,7 +76,7 @@ export class CreateEndpointCfStep1Component extends CreateEndpointHelperComponen // Optional Client ID and Client Secret clientIDField: ['', []], clientSecretField: ['', []], - createUserEndpointField: [false, []], + createSystemEndpointField: [true, []], }); const epType = getIdFromRoute(activatedRoute, 'type'); @@ -98,7 +99,7 @@ export class CreateEndpointCfStep1Component extends CreateEndpointHelperComponen this.registerForm.value.clientIDField, this.registerForm.value.clientSecretField, this.registerForm.value.ssoAllowedField, - this.registerForm.value.createUserEndpointField, + this.registerForm.value.createSystemEndpointField, ).pipe( pairwise(), filter(([oldVal, newVal]) => (oldVal.busy && !newVal.busy)), @@ -149,7 +150,7 @@ export class CreateEndpointCfStep1Component extends CreateEndpointHelperComponen this.showAdvancedOptions = !this.showAdvancedOptions; } - toggleCreateUserEndpoint() { + toggleCreateSystemEndpoint() { // wait a tick for validators to adjust to new data in the directive setTimeout(() => { this.registerForm.controls.nameField.updateValueAndValidity(); diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper.ts index 789a60e721..aa35943537 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-helper.ts @@ -5,6 +5,7 @@ import { getFullEndpointApiUrl } from '../../../../../store/src/endpoint-utils'; import { stratosEntityCatalog } from '../../../../../store/src/stratos-entity-catalog'; 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 { SessionService } from '../../../shared/services/session.service'; type EndpointObservable = Observable<{ @@ -22,7 +23,8 @@ export class CreateEndpointHelperComponent { constructor( public sessionService: SessionService, - public currentUserPermissionsService: CurrentUserPermissionsService + public currentUserPermissionsService: CurrentUserPermissionsService, + public userProfileService: UserProfileService ) { const currentPage$ = stratosEntityCatalog.endpoint.store.getAll.getPaginationMonitor().currentPage$; this.existingSystemEndpoints = currentPage$.pipe( @@ -31,10 +33,13 @@ export class CreateEndpointHelperComponent { urls: endpoints.filter(ep => ep.creator.system).map(ep => getFullEndpointApiUrl(ep)), })) ); - this.existingPersonalEndpoints = currentPage$.pipe( - map(endpoints => ({ - names: endpoints.filter(ep => !ep.creator.system).map(ep => ep.name), - urls: endpoints.filter(ep => !ep.creator.system).map(ep => getFullEndpointApiUrl(ep)), + this.existingPersonalEndpoints = combineLatest([ + currentPage$, + this.userProfileService.userProfile$ + ]).pipe( + map(([endpoints, profile]) => ({ + names: endpoints.filter(ep => !ep.creator.system && ep.creator.name === profile.userName).map(ep => ep.name), + urls: endpoints.filter(ep => !ep.creator.system && ep.creator.name === profile.userName).map(ep => getFullEndpointApiUrl(ep)), })) ); this.existingEndpoints = currentPage$.pipe( diff --git a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.html b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.html index 67198988be..ba6373518f 100644 --- a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.html +++ b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.html @@ -19,21 +19,21 @@

Select the type of {{gitTypes[epSubType].label}} to register

+ [appUnique]="registerForm.value.createSystemEndpointField ? (customEndpoints | async)?.names : (existingPersonalEndpoints | async)?.names"> Name is required Name is not unique + [appUnique]="registerForm.value.createSystemEndpointField ? (customEndpoints | async)?.urls : (existingPersonalEndpoints | async)?.urls"> URL is required Invalid API URL URL is not unique - Create a user endpoint (only visible to you and other admins) + Create a system endpoint (visible to all users) Skip SSL validation for the diff --git a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts index bfdbf8c956..2ab2d3bab8 100644 --- a/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts +++ b/src/frontend/packages/git/src/shared/components/git-registration/git-registration.component.ts @@ -13,6 +13,7 @@ import { ConnectEndpointConfig } from '../../../../../core/src/features/endpoint import { StepOnNextFunction } from '../../../../../core/src/shared/components/stepper/step/step.component'; import { SessionService } from '../../../../../core/src/shared/services/session.service'; import { CurrentUserPermissionsService } from '../../../../../core/src/core/permissions/current-user-permissions.service'; +import { UserProfileService } from '../../../../../core/src/core/user-profile.service'; import { SnackBarService } from '../../../../../core/src/shared/services/snackbar.service'; import { getFullEndpointApiUrl } from '../../../../../store/src/endpoint-utils'; import { entityCatalog } from '../../../../../store/src/public-api'; @@ -77,9 +78,10 @@ export class GitRegistrationComponent extends CreateEndpointHelperComponent impl private snackBarService: SnackBarService, private endpointsService: EndpointsService, public sessionService: SessionService, - public currentUserPermissionsService: CurrentUserPermissionsService + public currentUserPermissionsService: CurrentUserPermissionsService, + public userProfileService: UserProfileService ) { - super(sessionService, currentUserPermissionsService); + super(sessionService, currentUserPermissionsService, userProfileService); this.epSubType = getIdFromRoute(activatedRoute, 'subtype'); const githubLabel = entityCatalog.getEndpoint(GIT_ENDPOINT_TYPE, GIT_ENDPOINT_SUB_TYPES.GITHUB).definition.label || 'Github'; const gitlabLabel = entityCatalog.getEndpoint(GIT_ENDPOINT_TYPE, GIT_ENDPOINT_SUB_TYPES.GITLAB).definition.label || 'Gitlab'; @@ -159,7 +161,7 @@ export class GitRegistrationComponent extends CreateEndpointHelperComponent impl nameField: ['', [Validators.required]], urlField: ['', [Validators.required]], skipSllField: [false, []], - createUserEndpointField: [false, []], + createSystemEndpointField: [true, []], }); this.updateType(); @@ -201,10 +203,10 @@ export class GitRegistrationComponent extends CreateEndpointHelperComponent impl const skipSSL = this.registerForm.controls.nameField.value && this.registerForm.controls.urlField.value ? this.registerForm.controls.skipSllField.value : false; - const createUserEndpoint = this.registerForm.controls.createUserEndpointField.value; + const createSystemEndpoint = this.registerForm.controls.createSystemEndpointField.value; return stratosEntityCatalog.endpoint.api.register(GIT_ENDPOINT_TYPE, - this.epSubType, name, url, skipSSL, '', '', false, createUserEndpoint) + this.epSubType, name, url, skipSSL, '', '', false, createSystemEndpoint) .pipe( pairwise(), filter(([oldVal, newVal]) => (oldVal.busy && !newVal.busy)), @@ -240,7 +242,7 @@ export class GitRegistrationComponent extends CreateEndpointHelperComponent impl return ready + '/' + defn.urlSuffix; } - toggleCreateUserEndpoint() { + toggleCreateSystemEndpoint() { // wait a tick for validators to adjust to new data in the directive setTimeout(() => { this.registerForm.controls.nameField.updateValueAndValidity(); diff --git a/src/frontend/packages/store/src/actions/endpoint.actions.ts b/src/frontend/packages/store/src/actions/endpoint.actions.ts index 2d93409c39..372da47c61 100644 --- a/src/frontend/packages/store/src/actions/endpoint.actions.ts +++ b/src/frontend/packages/store/src/actions/endpoint.actions.ts @@ -215,7 +215,7 @@ export class RegisterEndpoint extends SingleBaseEndpointAction { public clientID = '', public clientSecret = '', public ssoAllowed: boolean, - public createUserEndpoint: boolean, + public createSystemEndpoint: boolean, ) { super( REGISTER_ENDPOINTS, diff --git a/src/frontend/packages/store/src/effects/endpoint.effects.ts b/src/frontend/packages/store/src/effects/endpoint.effects.ts index e51ef30fa2..fff7653c04 100644 --- a/src/frontend/packages/store/src/effects/endpoint.effects.ts +++ b/src/frontend/packages/store/src/effects/endpoint.effects.ts @@ -195,7 +195,7 @@ export class EndpointsEffect { cnsi_client_id: action.clientID, cnsi_client_secret: action.clientSecret, sso_allowed: action.ssoAllowed ? 'true' : 'false', - create_user_endpoint: action.createUserEndpoint ? 'true' : 'false' + create_system_endpoint: action.createSystemEndpoint ? 'true' : 'false' }; // Do not include sub_type in HttpParams if it doesn't exist (falsies get stringified and sent) if (action.endpointSubType) { diff --git a/src/frontend/packages/store/src/stratos-action-builders.ts b/src/frontend/packages/store/src/stratos-action-builders.ts index 57d2dfe19b..5bad71e652 100644 --- a/src/frontend/packages/store/src/stratos-action-builders.ts +++ b/src/frontend/packages/store/src/stratos-action-builders.ts @@ -60,7 +60,7 @@ export interface EndpointActionBuilder extends OrchestratedActionBuilders { clientID?: string, clientSecret?: string, ssoAllowed?: boolean, - createUserEndpointField?: boolean, + createSystemEndpointField?: boolean, ) => RegisterEndpoint; update: ( guid: string, @@ -105,7 +105,7 @@ export const endpointActionBuilder: EndpointActionBuilder = { clientID?: string, clientSecret?: string, ssoAllowed?: boolean, - createUserEndpoint?: boolean, + createSystemEndpoint?: boolean, ) => new RegisterEndpoint( endpointType, endpointSubType, @@ -115,7 +115,7 @@ export const endpointActionBuilder: EndpointActionBuilder = { clientID, clientSecret, ssoAllowed, - createUserEndpoint, + createSystemEndpoint, ), update: ( guid: string, diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 2cd160eb45..909bd85cb0 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -63,10 +63,10 @@ func (p *portalProxy) RegisterEndpoint(c echo.Context, fetchInfo interfaces.Info cnsiClientSecret := params.CNSIClientSecret subType := params.SubType - createUserEndpoint, err := strconv.ParseBool(params.CreateUserEndpoint) + createSystemEndpoint, err := strconv.ParseBool(params.CreateSystemEndpoint) if err != nil { - // default to false - createUserEndpoint = false + // default to true + createSystemEndpoint = true } if cnsiClientId == "" { @@ -82,7 +82,7 @@ func (p *portalProxy) RegisterEndpoint(c echo.Context, fetchInfo interfaces.Info "Failed to get session user: %v", err) } - newCNSI, err := p.DoRegisterEndpoint(params.CNSIName, params.APIEndpoint, skipSSLValidation, cnsiClientId, cnsiClientSecret, userID, ssoAllowed, subType, createUserEndpoint, fetchInfo) + newCNSI, err := p.DoRegisterEndpoint(params.CNSIName, params.APIEndpoint, skipSSLValidation, cnsiClientId, cnsiClientSecret, userID, ssoAllowed, subType, createSystemEndpoint, fetchInfo) if err != nil { return err } @@ -91,7 +91,7 @@ func (p *portalProxy) RegisterEndpoint(c echo.Context, fetchInfo interfaces.Info return nil } -func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, skipSSLValidation bool, clientId string, clientSecret string, userId string, ssoAllowed bool, subType string, createUserEndpoint bool, fetchInfo interfaces.InfoFunc) (interfaces.CNSIRecord, error) { +func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, skipSSLValidation bool, clientId string, clientSecret string, userId string, ssoAllowed bool, subType string, createSystemEndpoint bool, fetchInfo interfaces.InfoFunc) (interfaces.CNSIRecord, error) { log.Debug("DoRegisterEndpoint") if len(cnsiName) == 0 || len(apiEndpoint) == 0 { @@ -150,7 +150,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk // check if we've already got this APIEndpoint in this DB for _, duplicate := range duplicateEndpoints { // cant create same system endpoint - if len(duplicate.Creator) == 0 && isAdmin && !createUserEndpoint { + if len(duplicate.Creator) == 0 && isAdmin && createSystemEndpoint { return interfaces.CNSIRecord{}, interfaces.NewHTTPShadowError( http.StatusBadRequest, "Can not register same system endpoint multiple times", @@ -171,7 +171,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk h := sha1.New() // see why its generated this way in Issue #4753 / #3031 - if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled && (!isAdmin || createUserEndpoint) { + if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled && (!isAdmin || (isAdmin && !createSystemEndpoint)) { // Make the new guid unique per api url AND user id h.Write([]byte(apiEndpointURL.String() + userId)) } else { @@ -204,7 +204,7 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk newCNSI.SubType = subType // admins currently can't create user endpoints - if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled && (!isAdmin || createUserEndpoint) { + if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled && (!isAdmin || !createSystemEndpoint) { newCNSI.Creator = userId } diff --git a/src/jetstream/cnsi_test.go b/src/jetstream/cnsi_test.go index 393f8ba93d..a365890d3a 100644 --- a/src/jetstream/cnsi_test.go +++ b/src/jetstream/cnsi_test.go @@ -307,9 +307,9 @@ func TestRegisterWithUserEndpointsEnabled(t *testing.T) { pp.GetConfig().UserEndpointsEnabled = config.UserEndpointsConfigEnum.Enabled Convey("as admin", func() { - Convey("with createUserEndpoint disabled", func() { + Convey("with createSystemEndpoint enabled", func() { // setup - adminEndpoint := setupMockEndpointRegisterRequest(t, mockAdmin.ConnectedUser, mockV2Info[0], "CF Cluster 1", false, true) + adminEndpoint := setupMockEndpointRegisterRequest(t, mockAdmin.ConnectedUser, mockV2Info[0], "CF Cluster 1", true, true) if errSession := pp.setSessionValues(adminEndpoint.EchoContext, mockAdmin.SessionValues); errSession != nil { t.Error(errors.New("unable to mock/stub user in session object")) @@ -400,11 +400,11 @@ func TestRegisterWithUserEndpointsEnabled(t *testing.T) { }) }) }) - Convey("with createUserEndpoint enabled", func() { + Convey("with createSystemEndpoint disabled", func() { // setup - adminEndpoint := setupMockEndpointRegisterRequest(t, mockAdmin.ConnectedUser, mockV2Info[0], "CF Cluster 1", true, false) - systemEndpoint := setupMockEndpointRegisterRequest(t, mockAdmin.ConnectedUser, mockV2Info[0], "CF Cluster 1", true, true) + adminEndpoint := setupMockEndpointRegisterRequest(t, mockAdmin.ConnectedUser, mockV2Info[0], "CF Cluster 1", false, false) + systemEndpoint := setupMockEndpointRegisterRequest(t, mockAdmin.ConnectedUser, mockV2Info[0], "CF Cluster 1", false, true) if errSession := pp.setSessionValues(adminEndpoint.EchoContext, mockAdmin.SessionValues); errSession != nil { t.Error(errors.New("unable to mock/stub user in session object")) @@ -469,9 +469,9 @@ func TestRegisterWithUserEndpointsEnabled(t *testing.T) { }) Convey("as user", func() { - Convey("with createUserEndpoint disabled", func() { + Convey("with createSystemEndpoint enabled", func() { // setup - userEndpoint := setupMockEndpointRegisterRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1", false, false) + userEndpoint := setupMockEndpointRegisterRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1", true, false) if errSession := pp.setSessionValues(userEndpoint.EchoContext, mockUser1.SessionValues); errSession != nil { t.Error(errors.New("unable to mock/stub user in session object")) @@ -555,8 +555,8 @@ func TestRegisterWithUserEndpointsEnabled(t *testing.T) { }) }) }) - Convey("with createUserEndpoint enabled", func() { - userEndpoint := setupMockEndpointRegisterRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1", true, false) + Convey("with createSystemEndpoint disabled", func() { + userEndpoint := setupMockEndpointRegisterRequest(t, mockUser1.ConnectedUser, mockV2Info[0], "CF Cluster 1", false, false) if errSession := pp.setSessionValues(userEndpoint.EchoContext, mockUser1.SessionValues); errSession != nil { t.Error(errors.New("unable to mock/stub user in session object")) diff --git a/src/jetstream/mock_server_test.go b/src/jetstream/mock_server_test.go index 8e96ff2724..93d1fe8b48 100644 --- a/src/jetstream/mock_server_test.go +++ b/src/jetstream/mock_server_test.go @@ -272,16 +272,16 @@ func setupMockUser(guid string, admin bool, scopes []string) MockUser { } // mockV2Info needs to be closed -func setupMockEndpointRegisterRequest(t *testing.T, user *interfaces.ConnectedUser, mockV2Info *httptest.Server, endpointName string, createUserEndpoint bool, generateAdminGUID bool) MockEndpointRequest { +func setupMockEndpointRegisterRequest(t *testing.T, user *interfaces.ConnectedUser, mockV2Info *httptest.Server, endpointName string, createSystemEndpoint bool, generateAdminGUID bool) MockEndpointRequest { // create a request for each endpoint req := setupMockReq("POST", "", map[string]string{ - "cnsi_name": endpointName, - "api_endpoint": mockV2Info.URL, - "skip_ssl_validation": "true", - "cnsi_client_id": mockClientId, - "cnsi_client_secret": mockClientSecret, - "create_user_endpoint": strconv.FormatBool(createUserEndpoint), + "cnsi_name": endpointName, + "api_endpoint": mockV2Info.URL, + "skip_ssl_validation": "true", + "cnsi_client_id": mockClientId, + "cnsi_client_secret": mockClientSecret, + "create_system_endpoint": strconv.FormatBool(createSystemEndpoint), }) res := httptest.NewRecorder() diff --git a/src/jetstream/repository/interfaces/portal_proxy.go b/src/jetstream/repository/interfaces/portal_proxy.go index 8b84ced2fc..dcc4e8f630 100644 --- a/src/jetstream/repository/interfaces/portal_proxy.go +++ b/src/jetstream/repository/interfaces/portal_proxy.go @@ -14,7 +14,7 @@ type PortalProxy interface { GetHttpClient(skipSSLValidation bool) http.Client GetHttpClientForRequest(req *http.Request, skipSSLValidation bool) http.Client RegisterEndpoint(c echo.Context, fetchInfo InfoFunc) error - DoRegisterEndpoint(cnsiName string, apiEndpoint string, skipSSLValidation bool, clientId string, clientSecret string, userId string, ssoAllowed bool, subType string, createUserEndpoint bool, fetchInfo InfoFunc) (CNSIRecord, error) + DoRegisterEndpoint(cnsiName string, apiEndpoint string, skipSSLValidation bool, clientId string, clientSecret string, userId string, ssoAllowed bool, subType string, createSystemEndpoint bool, fetchInfo InfoFunc) (CNSIRecord, error) GetEndpointTypeSpec(typeName string) (EndpointPlugin, error) // Auth diff --git a/src/jetstream/repository/interfaces/structs.go b/src/jetstream/repository/interfaces/structs.go index 66d38e25f1..d057698ac7 100644 --- a/src/jetstream/repository/interfaces/structs.go +++ b/src/jetstream/repository/interfaces/structs.go @@ -426,15 +426,15 @@ type LoginToCNSIParams struct { } type RegisterEndpointParams struct { - EndpointType string `json:"endpoint_type" form:"endpoint_type" query:"endpoint_type"` - CNSIName string `json:"cnsi_name" form:"cnsi_name" query:"cnsi_name"` - APIEndpoint string `json:"api_endpoint" form:"api_endpoint" query:"api_endpoint"` - SkipSSLValidation string `json:"skip_ssl_validation" form:"skip_ssl_validation" query:"skip_ssl_validation"` - SSOAllowed string `json:"sso_allowed" form:"sso_allowed" query:"sso_allowed"` - CNSIClientID string `json:"cnsi_client_id" form:"cnsi_client_id" query:"cnsi_client_id"` - CNSIClientSecret string `json:"cnsi_client_secret" form:"cnsi_client_secret" query:"cnsi_client_secret"` - SubType string `json:"sub_type" form:"sub_type" query:"sub_type"` - CreateUserEndpoint string `json:"create_user_endpoint" form:"create_user_endpoint" query:"create_user_endpoint"` + EndpointType string `json:"endpoint_type" form:"endpoint_type" query:"endpoint_type"` + CNSIName string `json:"cnsi_name" form:"cnsi_name" query:"cnsi_name"` + APIEndpoint string `json:"api_endpoint" form:"api_endpoint" query:"api_endpoint"` + SkipSSLValidation string `json:"skip_ssl_validation" form:"skip_ssl_validation" query:"skip_ssl_validation"` + SSOAllowed string `json:"sso_allowed" form:"sso_allowed" query:"sso_allowed"` + CNSIClientID string `json:"cnsi_client_id" form:"cnsi_client_id" query:"cnsi_client_id"` + CNSIClientSecret string `json:"cnsi_client_secret" form:"cnsi_client_secret" query:"cnsi_client_secret"` + SubType string `json:"sub_type" form:"sub_type" query:"sub_type"` + CreateSystemEndpoint string `json:"create_system_endpoint" form:"create_system_endpoint" query:"create_system_endpoint"` } type UpdateEndpointParams struct { From 329fdc59736d70de0832e2c1fa0a577c0703fae4 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Tue, 13 Apr 2021 11:30:38 +0200 Subject: [PATCH 098/101] Update documentation (#4876) Signed-off-by: Thomas Quandt --- deploy/kubernetes/console/values.schema.json | 2 +- website/docs/endpoints/cf/user-endpoints.md | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/deploy/kubernetes/console/values.schema.json b/deploy/kubernetes/console/values.schema.json index 52c39f5ec8..9ff9cfeb90 100644 --- a/deploy/kubernetes/console/values.schema.json +++ b/deploy/kubernetes/console/values.schema.json @@ -17,7 +17,7 @@ "userEndpointsEnabled": { "type": "string", "enum": ["disabled", "admin_only", "enabled"], - "description": "Enable, disable or let only admins view user endpoints" + "description": "Enable, disable or let only admins view and create user endpoints" }, "autoRegisterCF": { "type": ["string", "null"] diff --git a/website/docs/endpoints/cf/user-endpoints.md b/website/docs/endpoints/cf/user-endpoints.md index 5c7b542000..2b08913049 100644 --- a/website/docs/endpoints/cf/user-endpoints.md +++ b/website/docs/endpoints/cf/user-endpoints.md @@ -5,7 +5,7 @@ sidebar_label: Configuring User Endpoints Stratos provides a way for users to create endpoints without the need to be an administrator. -> Note: Admin endpoint-ID's are generated through a SHA-1 encryption of the URL. Endpoints created by a user will differ in their ID, by using the URL + user-ID for encryption. This should pose no problem in the usual Stratos workflow, but if you depend on the ID to be based solely on the URL, then use this feature with caution. +> Note: Admin endpoint-ID's are generated through a SHA-1 encryption of the URL. Personal endpoints will differ in their ID, by using the URL + user-ID for encryption. This should pose no problem in the usual Stratos workflow, but if you depend on the ID to be based solely on the URL, then use this feature with caution. ## Set up @@ -15,16 +15,15 @@ In order to enable User Endpoints support in Stratos: 2. The UAA client used by Stratos needs an additional scope `stratos.endpointadmin` 3. Users need to have the `stratos.endpointadmin` group attached to them -Once all steps have been completed, user within the `stratos.endpointadmin` group are allowed to create endpoints. Endpoints created by users are only visible to their respective user and all admins. +Once all steps have been completed, user within the `stratos.endpointadmin` group are allowed to create personal user endpoints. Endpoints created that way are only visible to their respective user and all admins. Admins will be able to create personal user endpoints after step 1 has been completed. ## Environment variable `USER_ENDPOINTS_ENABLED` or helm chart value `console.userEndpointsEnabled` can be set to three different states: 1. `disabled` (default) will disable this feature. Neither admins nor users will see user endpoints. -2. `admin_only` will hide user endpoints from users. Admins can still see all endpoints created by users. -3. `enabled` will allow users within the `stratos.endpointadmin` group to create endpoints. The endpoints will only be visible to them or admins. - +2. `admin_only` will hide user endpoints from users. Admins can create and see all user endpoints. +3. `enabled` will allow users within the `stratos.endpointadmin` group and admins to create personal user endpoints. These endpoints will only be visible to them or admins. ## Adding scopes to the UAA client From 7fdd7dd421b15ca4acee6dc7ae6606c31ee14aa9 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 15 Apr 2021 15:46:41 +0200 Subject: [PATCH 099/101] Change creator visibility and fix permission bug (#4876) Signed-off-by: Thomas Quandt --- .../endpoint/endpoint-card/endpoint-card.component.ts | 5 +++-- .../list/list-types/endpoint/endpoint-list.helpers.ts | 4 ++-- .../list-types/endpoint/endpoints-list-config.service.ts | 6 ++++-- src/jetstream/cnsi.go | 1 - src/jetstream/info.go | 2 +- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.ts index 0f002408dd..06f27b7fe6 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-card/endpoint-card.component.ts @@ -140,9 +140,10 @@ export class EndpointCardComponent extends CardCell implements On this.sessionService.userEndpointsEnabled(), this.sessionService.userEndpointsNotDisabled(), this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT), + this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT) ]).pipe( - map(([userEndpointsEnabled, userEndpointsNotDisabled, isAdmin]) => { - return userEndpointsEnabled || (userEndpointsNotDisabled && isAdmin); + map(([userEndpointsEnabled, userEndpointsNotDisabled, isAdmin, isEndpointAdmin]) => { + return (userEndpointsEnabled && (isAdmin || isEndpointAdmin)) || (userEndpointsNotDisabled && isAdmin); }) ); } diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts index f65ac5ef15..25b9d8d5ca 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts @@ -183,7 +183,7 @@ export class EndpointListHelper { row$ ]).pipe( map(([userEndpointsEnabled, isAdmin, isEndpointAdmin, row]) => { - if (!userEndpointsEnabled || row.creator.admin) { + if (!userEndpointsEnabled || row.creator.system) { return isAdmin; } else { return isEndpointAdmin || isAdmin; @@ -207,7 +207,7 @@ export class EndpointListHelper { row$ ]).pipe( map(([userEndpointsEnabled, isAdmin, isEndpointAdmin, row]) => { - if (!userEndpointsEnabled || row.creator.admin) { + if (!userEndpointsEnabled || row.creator.system) { return isAdmin; } else { return isEndpointAdmin || isAdmin; diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts index 53eea98d8e..ebdbf37e7e 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts @@ -130,9 +130,11 @@ export class EndpointsListConfigService implements IListConfig { sessionService.userEndpointsEnabled(), sessionService.userEndpointsNotDisabled(), currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT), + currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT) ]).pipe( - map(([userEndpointsEnabled, userEndpointsNotDisabled, isAdmin]) => { - return userEndpointsEnabled || (userEndpointsNotDisabled && isAdmin); + first(), + map(([userEndpointsEnabled, userEndpointsNotDisabled, isAdmin, isEndpointAdmin]) => { + return (userEndpointsEnabled && (isAdmin || isEndpointAdmin)) || (userEndpointsNotDisabled && isAdmin); }) ).subscribe(enabled => { if (enabled) { diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 909bd85cb0..ea1e95dcc4 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -203,7 +203,6 @@ func (p *portalProxy) DoRegisterEndpoint(cnsiName string, apiEndpoint string, sk newCNSI.SSOAllowed = ssoAllowed newCNSI.SubType = subType - // admins currently can't create user endpoints if p.GetConfig().UserEndpointsEnabled != config.UserEndpointsConfigEnum.Disabled && (!isAdmin || !createSystemEndpoint) { newCNSI.Creator = userId } diff --git a/src/jetstream/info.go b/src/jetstream/info.go index e449f2b67f..6bb90e00f8 100644 --- a/src/jetstream/info.go +++ b/src/jetstream/info.go @@ -102,7 +102,7 @@ func (p *portalProxy) getInfo(c echo.Context) (*interfaces.Info, error) { // set the creator preemptively as admin, if no id is found endpoint.Creator = &interfaces.CreatorInfo{ - Name: "system", + Name: "Global Endpoint", Admin: false, System: true, } From 0757fba76e3c58b425b13685e219f8eab1604d28 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 15 Apr 2021 15:50:55 +0200 Subject: [PATCH 100/101] Move docs from endpoints/cf to advanced (#4876) Signed-off-by: Thomas Quandt --- website/docs/{endpoints/cf => advanced}/user-endpoints.md | 0 website/sidebars.js | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename website/docs/{endpoints/cf => advanced}/user-endpoints.md (100%) diff --git a/website/docs/endpoints/cf/user-endpoints.md b/website/docs/advanced/user-endpoints.md similarity index 100% rename from website/docs/endpoints/cf/user-endpoints.md rename to website/docs/advanced/user-endpoints.md diff --git a/website/sidebars.js b/website/sidebars.js index 251a3d4a96..129d1680a2 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -31,8 +31,7 @@ module.exports = { { 'Cloud Foundry': [ 'endpoints/cf/cf', - 'endpoints/cf/invite-user-guide', - 'endpoints/cf/user-endpoints' + 'endpoints/cf/invite-user-guide' ] }, 'endpoints/k8s', @@ -46,6 +45,7 @@ module.exports = { ], 'Advanced Topics': [ 'advanced/sso', + 'advanced/user-endpoints' ], 'Develop': [ 'developer/contributing', From 043140c90cb06a0c817e67d5882fbb0c102a90e9 Mon Sep 17 00:00:00 2001 From: Thomas Quandt Date: Thu, 15 Apr 2021 16:00:06 +0200 Subject: [PATCH 101/101] Rename global endpoints and update config file comments (#4876) Signed-off-by: Thomas Quandt --- src/jetstream/config.dev | 2 +- src/jetstream/config.example | 2 +- src/jetstream/info.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/jetstream/config.dev b/src/jetstream/config.dev index 522ec73ce9..4087f02267 100644 --- a/src/jetstream/config.dev +++ b/src/jetstream/config.dev @@ -58,7 +58,7 @@ LOCAL_USER_PASSWORD=admin LOCAL_USER_SCOPE=stratos.admin # Enable users create user endpoints (disabled, admin_only, enabled). Default is disabled -# admin_only will enable admins to see created user endpoints, but users won't be able to create user endpoints or see them +# admin_only will enable admins to create or see user endpoints, but users won't be able to create user endpoints or see them USER_ENDPOINTS_ENABLED=disabled # Enable/disable API key-based access to Stratos API (disabled, admin_only, all_users). Default is admin_only diff --git a/src/jetstream/config.example b/src/jetstream/config.example index 2c31f6c33e..0b9de1830b 100644 --- a/src/jetstream/config.example +++ b/src/jetstream/config.example @@ -40,7 +40,7 @@ ENABLE_TECH_PREVIEW=false # HOME_VIEW_SHOW_FAVORITES_ONLY=false # Enable users create user endpoints (disabled, admin_only, enabled). Default is disabled -# admin_only will enable admins to see created user endpoints, but users won't be able to create user endpoints or see them +# admin_only will enable admins to create or see user endpoints, but users won't be able to create user endpoints or see them USER_ENDPOINTS_ENABLED=disabled # User Invites diff --git a/src/jetstream/info.go b/src/jetstream/info.go index 6bb90e00f8..5e00266a90 100644 --- a/src/jetstream/info.go +++ b/src/jetstream/info.go @@ -102,7 +102,7 @@ func (p *portalProxy) getInfo(c echo.Context) (*interfaces.Info, error) { // set the creator preemptively as admin, if no id is found endpoint.Creator = &interfaces.CreatorInfo{ - Name: "Global Endpoint", + Name: "System Endpoint", Admin: false, System: true, }