diff --git a/src/backend/app-core/auth.go b/src/backend/app-core/auth.go index 6c81169f9a..c7650e2068 100644 --- a/src/backend/app-core/auth.go +++ b/src/backend/app-core/auth.go @@ -150,9 +150,11 @@ func (p *portalProxy) loginToUAA(c echo.Context) error { } // Connect to the given Endpoint +// Note, an admin user can connect an endpoint as a system endpoint to share it with others func (p *portalProxy) loginToCNSI(c echo.Context) error { log.Debug("loginToCNSI") cnsiGuid := c.FormValue("cnsi_guid") + var systemSharedToken = false if len(cnsiGuid) == 0 { return interfaces.NewHTTPShadowError( @@ -161,7 +163,12 @@ func (p *portalProxy) loginToCNSI(c echo.Context) error { "Need Endpoint GUID passed as form param") } - resp, err := p.DoLoginToCNSI(c, cnsiGuid) + systemSharedValue := c.FormValue("system_shared") + if len(systemSharedValue) > 0 { + systemSharedToken = systemSharedValue == "true" + } + + resp, err := p.DoLoginToCNSI(c, cnsiGuid, systemSharedToken) if err != nil { return err } @@ -176,7 +183,7 @@ func (p *portalProxy) loginToCNSI(c echo.Context) error { return nil } -func (p *portalProxy) DoLoginToCNSI(c echo.Context, cnsiGUID string) (*interfaces.LoginRes, error) { +func (p *portalProxy) DoLoginToCNSI(c echo.Context, cnsiGUID string, systemSharedToken bool) (*interfaces.LoginRes, error) { cnsiRecord, err := p.GetCNSIRecord(cnsiGUID) if err != nil { @@ -192,6 +199,23 @@ func (p *portalProxy) DoLoginToCNSI(c echo.Context, cnsiGUID string) (*interface 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.GetUAAUser(userID) + if err != nil { + return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect System Shared endpoint - could not check user") + } + + if !user.Admin { + return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect System Shared endpoint - user is not an administrator") + } + + // We are all good to go - change the userID, so we record this token against the system-shared user and not this specific user + // This is how we identify system-shared endpoint tokens + userID = tokens.SystemSharedUserGuid + } + // Ask the endpoint type to connect for _, plugin := range p.Plugins { endpointPlugin, err := plugin.GetEndpointPlugin() @@ -227,6 +251,9 @@ func (p *portalProxy) DoLoginToCNSI(c echo.Context, cnsiGUID string) (*interface cnsiUser, ok := p.GetCNSIUserFromToken(cnsiGUID, tokenRecord) if ok { + // If this is a system shared endpoint, then remove some metadata that should be send back to other users + santizeInfoForSystemSharedTokenUser(cnsiUser, systemSharedToken) + resp.User = cnsiUser } @@ -240,6 +267,14 @@ func (p *portalProxy) DoLoginToCNSI(c echo.Context, cnsiGUID string) (*interface "Endpoint connection not supported") } +func santizeInfoForSystemSharedTokenUser(cnsiUser *interfaces.ConnectedUser, isSysystemShared bool) { + if isSysystemShared { + cnsiUser.GUID = tokens.SystemSharedUserGuid + cnsiUser.Scopes = make([]string, 0) + cnsiUser.Name = "system_shared" + } +} + func (p *portalProxy) ConnectOAuth2(c echo.Context, cnsiRecord interfaces.CNSIRecord) (*interfaces.TokenRecord, error) { uaaRes, u, _, err := p.FetchOAuth2Token(cnsiRecord, c) if err != nil { @@ -316,6 +351,21 @@ func (p *portalProxy) logoutOfCNSI(c echo.Context) error { return fmt.Errorf("Unable to load CNSI record: %s", err) } + // Get the existing token to see if it is connected as a system shared endpoint + tr, ok := p.GetCNSITokenRecord(cnsiGUID, userGUID) + if ok && tr.SystemShared { + // User needs to be an admin + user, err := p.GetUAAUser(userGUID) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, "Can not disconnect System Shared endpoint - could not check user") + } + + if !user.Admin { + return echo.NewHTTPError(http.StatusUnauthorized, "Can not disconnect System Shared endpoint - user is not an administrator") + } + userGUID = tokens.SystemSharedUserGuid + } + // If cnsi is cf AND cf is auto-register only clear the entry if cnsiRecord.CNSIType == "cf" && p.GetConfig().AutoRegisterCFUrl == cnsiRecord.APIEndpoint.String() { log.Debug("Setting token record as disconnected") @@ -716,7 +766,7 @@ func (p *portalProxy) handleSessionExpiryHeader(c echo.Context) error { return nil } -func (p *portalProxy) getUAAUser(userGUID string) (*interfaces.ConnectedUser, error) { +func (p *portalProxy) GetUAAUser(userGUID string) (*interfaces.ConnectedUser, error) { log.Debug("getUAAUser") // get the uaa token record @@ -766,6 +816,10 @@ func (p *portalProxy) GetCNSIUserAndToken(cnsiGUID string, userGUID string) (*in } cnsiUser, ok := p.GetCNSIUserFromToken(cnsiGUID, &cfTokenRecord) + + // If this is a system shared endpoint, then remove some metadata that should be send back to other users + santizeInfoForSystemSharedTokenUser(cnsiUser, cfTokenRecord.SystemShared) + return cnsiUser, &cfTokenRecord, ok } diff --git a/src/backend/app-core/info.go b/src/backend/app-core/info.go index 9bb63fe84f..813bdfc768 100644 --- a/src/backend/app-core/info.go +++ b/src/backend/app-core/info.go @@ -40,7 +40,7 @@ func (p *portalProxy) getInfo(c echo.Context) (*interfaces.Info, error) { return nil, errors.New("Could not find session user_id") } - uaaUser, err := p.getUAAUser(userGUID) + uaaUser, err := p.GetUAAUser(userGUID) if err != nil { return nil, errors.New("Could not load session user data") } @@ -68,14 +68,16 @@ func (p *portalProxy) getInfo(c echo.Context) (*interfaces.Info, error) { for _, cnsi := range cnsiList { // Extend the CNSI record endpoint := &interfaces.EndpointDetail{ - CNSIRecord: cnsi, - Metadata: make(map[string]string), + CNSIRecord: cnsi, + Metadata: make(map[string]string), + SystemSharedToken: false, } // try to get the user info for this cnsi for the user cnsiUser, token, ok := p.GetCNSIUserAndToken(cnsi.GUID, userGUID) if ok { endpoint.User = cnsiUser endpoint.TokenMetadata = token.Metadata + endpoint.SystemSharedToken = token.SystemShared } cnsiType := cnsi.CNSIType s.Endpoints[cnsiType][cnsi.GUID] = endpoint diff --git a/src/backend/app-core/middleware.go b/src/backend/app-core/middleware.go index d09000a18d..a45d1669e4 100644 --- a/src/backend/app-core/middleware.go +++ b/src/backend/app-core/middleware.go @@ -116,7 +116,7 @@ func (p *portalProxy) adminMiddleware(h echo.HandlerFunc) echo.HandlerFunc { if err == nil { // check their admin status in UAA - u, err := p.getUAAUser(userID.(string)) + u, err := p.GetUAAUser(userID.(string)) if err != nil { return c.NoContent(http.StatusUnauthorized) } diff --git a/src/backend/app-core/mock_server_test.go b/src/backend/app-core/mock_server_test.go index a1bd57b1b2..87fd5b57c7 100644 --- a/src/backend/app-core/mock_server_test.go +++ b/src/backend/app-core/mock_server_test.go @@ -18,6 +18,7 @@ import ( "github.com/SUSE/stratos-ui/repository/crypto" "github.com/SUSE/stratos-ui/repository/interfaces" + "github.com/SUSE/stratos-ui/repository/tokens" "github.com/SUSE/stratos-ui/plugins/cloudfoundry" ) @@ -63,6 +64,7 @@ const mockCNSIGUID = "some-guid-1234" const mockCFGUID = "some-cf-guid-1234" const mockCEGUID = "some-hce-guid-1234" const mockUserGUID = "asd-gjfg-bob" +const mockAdminGUID = tokens.SystemSharedUserGuid const mockURLString = "http://localhost:9999/some/fake/url/" @@ -168,15 +170,15 @@ func expectCFAndCERows() sqlmock.Rows { } func expectTokenRow() sqlmock.Rows { - return sqlmock.NewRows([]string{"auth_token", "refresh_token", "token_expiry", "disconnected", "auth_type", "meta_data"}). - AddRow(mockUAAToken, mockUAAToken, mockTokenExpiry, false, "OAuth2", "") + return sqlmock.NewRows([]string{"auth_token", "refresh_token", "token_expiry", "disconnected", "auth_type", "meta_data", "user_guid"}). + AddRow(mockUAAToken, mockUAAToken, mockTokenExpiry, false, "OAuth2", "", mockUserGUID) } func expectEncryptedTokenRow(mockEncryptionKey []byte) sqlmock.Rows { encryptedUaaToken, _ := crypto.EncryptToken(mockEncryptionKey, mockUAAToken) - return sqlmock.NewRows([]string{"auth_token", "refresh_token", "token_expiry", "disconnected", "auth_type", "meta_data"}). - AddRow(encryptedUaaToken, encryptedUaaToken, mockTokenExpiry, false, "OAuth2", "") + return sqlmock.NewRows([]string{"auth_token", "refresh_token", "token_expiry", "disconnected", "auth_type", "meta_data", "user_guid"}). + AddRow(encryptedUaaToken, encryptedUaaToken, mockTokenExpiry, false, "OAuth2", "", mockUserGUID) } func setupHTTPTest(req *http.Request) (*httptest.ResponseRecorder, *echo.Echo, echo.Context, *portalProxy, *sql.DB, sqlmock.Sqlmock) { diff --git a/src/backend/app-core/oauth_requests_test.go b/src/backend/app-core/oauth_requests_test.go index e3637bf262..2a3981068e 100644 --- a/src/backend/app-core/oauth_requests_test.go +++ b/src/backend/app-core/oauth_requests_test.go @@ -97,10 +97,10 @@ func TestDoOauthFlowRequestWithValidToken(t *testing.T) { // p.getCNSIRequestRecords(cnsiRequest) -> // p.getCNSITokenRecord(r.GUID, r.UserGUID) -> // tokenRepo.FindCNSIToken(cnsiGUID, userGUID) - expectedCNSITokenRow := sqlmock.NewRows([]string{"auth_token", "refresh_token", "token_expiry", "disconnected", "auth_type", "meta_data"}). - AddRow(encryptedToken, encryptedToken, tokenExpiration, false, "OAuth2", "") + expectedCNSITokenRow := sqlmock.NewRows([]string{"auth_token", "refresh_token", "token_expiry", "disconnected", "auth_type", "meta_data", "user_guid"}). + AddRow(encryptedToken, encryptedToken, tokenExpiration, false, "OAuth2", "", mockUserGUID) mock.ExpectQuery(selectAnyFromTokens). - WithArgs(mockCNSIGUID, mockUserGUID). + WithArgs(mockCNSIGUID, mockUserGUID, mockAdminGUID). WillReturnRows(expectedCNSITokenRow) // p.GetCNSIRecord(r.GUID) -> cnsiRepo.Find(guid) @@ -227,10 +227,10 @@ func TestDoOauthFlowRequestWithExpiredToken(t *testing.T) { // p.getCNSIRequestRecords(cnsiRequest) -> // p.getCNSITokenRecord(r.GUID, r.UserGUID) -> // tokenRepo.FindCNSIToken(cnsiGUID, userGUID) - expectedCNSITokenRow := sqlmock.NewRows([]string{"auth_token", "refresh_token", "token_expiry", "disconnected", "auth_type", "meta_data"}). - AddRow(encryptedUAAToken, encryptedUAAToken, tokenExpiration, false, "OAuth2", "") + expectedCNSITokenRow := sqlmock.NewRows([]string{"auth_token", "refresh_token", "token_expiry", "disconnected", "auth_type", "meta_data", "user_guid"}). + AddRow(encryptedUAAToken, encryptedUAAToken, tokenExpiration, false, "OAuth2", "", mockUserGUID) mock.ExpectQuery(selectAnyFromTokens). - WithArgs(mockCNSIGUID, mockUserGUID). + WithArgs(mockCNSIGUID, mockUserGUID, mockAdminGUID). WillReturnRows(expectedCNSITokenRow) // p.GetCNSIRecord(r.GUID) -> cnsiRepo.Find(guid) @@ -240,10 +240,10 @@ func TestDoOauthFlowRequestWithExpiredToken(t *testing.T) { WithArgs(mockCNSIGUID). WillReturnRows(expectedCNSIRecordRow) - expectedCNSITokenRecordRow := sqlmock.NewRows([]string{"auth_token", "refresh_token", "token_expiry", "disconnected", "auth_type", "meta_data"}). - AddRow(encryptedUAAToken, encryptedUAAToken, tokenExpiration, false, "OAuth2", "") + expectedCNSITokenRecordRow := sqlmock.NewRows([]string{"auth_token", "refresh_token", "token_expiry", "disconnected", "auth_type", "meta_data", "user_guid"}). + AddRow(encryptedUAAToken, encryptedUAAToken, tokenExpiration, false, "OAuth2", "", mockUserGUID) mock.ExpectQuery(selectAnyFromTokens). - WithArgs(mockCNSIGUID, mockUserGUID). + WithArgs(mockCNSIGUID, mockUserGUID, mockAdminGUID). WillReturnRows(expectedCNSITokenRecordRow) mock.ExpectQuery(selectAnyFromTokens). @@ -370,10 +370,10 @@ func TestDoOauthFlowRequestWithFailedRefreshMethod(t *testing.T) { // p.getCNSIRequestRecords(cnsiRequest) -> // p.getCNSITokenRecord(r.GUID, r.UserGUID) -> // tokenRepo.FindCNSIToken(cnsiGUID, userGUID) - expectedCNSITokenRow := sqlmock.NewRows([]string{"auth_token", "refresh_token", "token_expiry", "disconnected", "auth_type", "meta_data"}). - AddRow(encryptedUAAToken, encryptedUAAToken, tokenExpiration, false, "OAuth2", "") + expectedCNSITokenRow := sqlmock.NewRows([]string{"auth_token", "refresh_token", "token_expiry", "disconnected", "auth_type", "meta_data", "user_guid"}). + AddRow(encryptedUAAToken, encryptedUAAToken, tokenExpiration, false, "OAuth2", "", mockUserGUID) mock.ExpectQuery(selectAnyFromTokens). - WithArgs(mockCNSIGUID, mockUserGUID). + WithArgs(mockCNSIGUID, mockUserGUID, mockAdminGUID). WillReturnRows(expectedCNSITokenRow) // p.GetCNSIRecord(r.GUID) -> cnsiRepo.Find(guid) @@ -617,7 +617,7 @@ func TestRefreshTokenWithDatabaseErrorOnSave(t *testing.T) { expectedCNSITokenRow := sqlmock.NewRows([]string{"auth_token", "refresh_token", "token_expiry", "disconnected", "auth_type", "meta_data"}). AddRow(mockUAAToken, mockUAAToken, tokenExpiration, false, "OAuth2", "") mock.ExpectQuery(selectAnyFromTokens). - WithArgs(mockCNSIGUID, mockUserGUID). + WithArgs(mockCNSIGUID, mockUserGUID, mockAdminGUID). WillReturnRows(expectedCNSITokenRow) // p.GetCNSIRecord(r.GUID) -> cnsiRepo.Find(guid) @@ -630,11 +630,11 @@ func TestRefreshTokenWithDatabaseErrorOnSave(t *testing.T) { expectedCNSITokenRecordRow := sqlmock.NewRows([]string{"auth_token", "refresh_token", "token_expiry", "disconnected", "auth_type", "meta_data"}). AddRow(mockUAAToken, mockUAAToken, tokenExpiration, false, "OAuth2", "") mock.ExpectQuery(selectAnyFromTokens). - WithArgs(mockCNSIGUID, mockUserGUID). + WithArgs(mockCNSIGUID, mockUserGUID, mockAdminGUID). WillReturnRows(expectedCNSITokenRecordRow) mock.ExpectQuery(selectAnyFromTokens). - WithArgs(mockCNSIGUID, mockUserGUID). + WithArgs(mockCNSIGUID, mockUserGUID, mockAdminGUID). WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow("1")) // p.saveCNSIToken(cnsiGUID, *u, uaaRes.AccessToken, uaaRes.RefreshToken) diff --git a/src/backend/app-core/passthrough_test.go b/src/backend/app-core/passthrough_test.go index 9ced176049..bc2fda8337 100644 --- a/src/backend/app-core/passthrough_test.go +++ b/src/backend/app-core/passthrough_test.go @@ -72,7 +72,7 @@ func TestPassthroughDoRequest(t *testing.T) { // p.getCNSITokenRecord(r.GUID, r.UserGUID) -> // tokenRepo.FindCNSIToken(cnsiGUID, userGUID) mock.ExpectQuery(selectAnyFromTokens). - WithArgs(mockCFGUID, mockUserGUID). + WithArgs(mockCFGUID, mockUserGUID, mockAdminGUID). WillReturnRows(expectEncryptedTokenRow(pp.Config.EncryptionKeyInBytes)) // p.GetCNSIRecord(r.GUID) -> cnsiRepo.Find(guid) @@ -81,7 +81,7 @@ func TestPassthroughDoRequest(t *testing.T) { WillReturnRows(expectCFRow()) mock.ExpectQuery(selectAnyFromTokens). - WithArgs(mockCFGUID, mockUserGUID). + WithArgs(mockCFGUID, mockUserGUID, mockAdminGUID). WillReturnRows(expectEncryptedTokenRow(pp.Config.EncryptionKeyInBytes)) // p.GetCNSIRecord(r.GUID) -> cnsiRepo.Find(guid) diff --git a/src/backend/app-core/repository/interfaces/portal_proxy.go b/src/backend/app-core/repository/interfaces/portal_proxy.go index d95505bb0d..610a0187aa 100644 --- a/src/backend/app-core/repository/interfaces/portal_proxy.go +++ b/src/backend/app-core/repository/interfaces/portal_proxy.go @@ -30,7 +30,7 @@ type PortalProxy interface { SaveConsoleConfig(consoleConfig *ConsoleConfig, consoleRepoInterface interface{}) error RefreshOAuthToken(skipSSLValidation bool, cnsiGUID, userGUID, client, clientSecret, tokenEndpoint string) (t TokenRecord, err error) - DoLoginToCNSI(c echo.Context, cnsiGUID string) (*LoginRes, error) + DoLoginToCNSI(c echo.Context, cnsiGUID string, systemSharedToken bool) (*LoginRes, error) // Expose internal portal proxy records to extensions GetCNSIRecord(guid string) (CNSIRecord, error) GetCNSIRecordByEndpoint(endpoint string) (CNSIRecord, error) @@ -49,6 +49,7 @@ type PortalProxy interface { GetUsername(userid string) (string, error) RefreshUAALogin(username, password string, store bool) error GetUserTokenInfo(tok string) (u *JWTUserTokenInfo, err error) + GetUAAUser(userGUID string) (*ConnectedUser, error) // Proxy API requests ProxyRequest(c echo.Context, uri *url.URL) (map[string]*CNSIRequest, error) diff --git a/src/backend/app-core/repository/interfaces/structs.go b/src/backend/app-core/repository/interfaces/structs.go index 66d1c0964a..34f0dfe547 100644 --- a/src/backend/app-core/repository/interfaces/structs.go +++ b/src/backend/app-core/repository/interfaces/structs.go @@ -71,6 +71,7 @@ type TokenRecord struct { Disconnected bool AuthType string Metadata string + SystemShared bool } type CFInfo struct { @@ -144,9 +145,10 @@ type Info struct { // Extends CNSI Record and adds the user type EndpointDetail struct { *CNSIRecord - User *ConnectedUser `json:"user"` - Metadata map[string]string `json:"metadata,omitempty"` - TokenMetadata string `json:"-"` + User *ConnectedUser `json:"user"` + Metadata map[string]string `json:"metadata,omitempty"` + TokenMetadata string `json:"-"` + SystemSharedToken bool `json:"system_shared_token"` } // Versions - response returned to caller from a getVersions action diff --git a/src/backend/app-core/repository/tokens/pgsql_tokens.go b/src/backend/app-core/repository/tokens/pgsql_tokens.go index be78a375fe..0f073240b2 100644 --- a/src/backend/app-core/repository/tokens/pgsql_tokens.go +++ b/src/backend/app-core/repository/tokens/pgsql_tokens.go @@ -26,13 +26,13 @@ var updateAuthToken = `UPDATE tokens SET auth_token = $1, refresh_token = $2, token_expiry = $3 WHERE user_guid = $4 AND token_type = $5` -var findCNSIToken = `SELECT auth_token, refresh_token, token_expiry, disconnected, auth_type, meta_data +var findCNSIToken = `SELECT auth_token, refresh_token, token_expiry, disconnected, auth_type, meta_data, user_guid FROM tokens - WHERE cnsi_guid = $1 AND user_guid = $2 AND token_type = 'cnsi'` + WHERE cnsi_guid = $1 AND (user_guid = $2 OR user_guid = $3) AND token_type = 'cnsi'` -var findCNSITokenConnected = `SELECT auth_token, refresh_token, token_expiry, disconnected, auth_type, meta_data +var findCNSITokenConnected = `SELECT auth_token, refresh_token, token_expiry, disconnected, auth_type, meta_data, user_guid FROM tokens - WHERE cnsi_guid = $1 AND user_guid = $2 AND token_type = 'cnsi' AND disconnected = '0'` + WHERE cnsi_guid = $1 AND (user_guid = $2 OR user_guid = $3) AND token_type = 'cnsi' AND disconnected = '0'` var countCNSITokens = `SELECT COUNT(*) FROM tokens @@ -304,13 +304,14 @@ func (p *PgsqlTokenRepository) findCNSIToken(cnsiGUID string, userGUID string, e disconnected bool authType string metadata string + tokenUserGUID string ) var err error if includeDisconnected { - err = p.db.QueryRow(findCNSIToken, cnsiGUID, userGUID).Scan(&ciphertextAuthToken, &ciphertextRefreshToken, &tokenExpiry, &disconnected, &authType, &metadata) + err = p.db.QueryRow(findCNSIToken, cnsiGUID, userGUID, SystemSharedUserGuid).Scan(&ciphertextAuthToken, &ciphertextRefreshToken, &tokenExpiry, &disconnected, &authType, &metadata, &tokenUserGUID) } else { - err = p.db.QueryRow(findCNSITokenConnected, cnsiGUID, userGUID).Scan(&ciphertextAuthToken, &ciphertextRefreshToken, &tokenExpiry, &disconnected, &authType, &metadata) + err = p.db.QueryRow(findCNSITokenConnected, cnsiGUID, userGUID, SystemSharedUserGuid).Scan(&ciphertextAuthToken, &ciphertextRefreshToken, &tokenExpiry, &disconnected, &authType, &metadata, &tokenUserGUID) } if err != nil { @@ -339,6 +340,7 @@ func (p *PgsqlTokenRepository) findCNSIToken(cnsiGUID string, userGUID string, e tr.Disconnected = disconnected tr.AuthType = authType tr.Metadata = metadata + tr.SystemShared = tokenUserGUID == SystemSharedUserGuid return *tr, nil } diff --git a/src/backend/app-core/repository/tokens/pgsql_tokens_test.go b/src/backend/app-core/repository/tokens/pgsql_tokens_test.go index 8fb6719d34..5d8ba10a47 100644 --- a/src/backend/app-core/repository/tokens/pgsql_tokens_test.go +++ b/src/backend/app-core/repository/tokens/pgsql_tokens_test.go @@ -20,7 +20,7 @@ const ( countTokensSql = `SELECT COUNT` insertTokenSql = `INSERT INTO tokens` updateUAATokenSql = `UPDATE tokens` - findTokenSql = `SELECT auth_token, refresh_token, token_expiry, disconnected, auth_type, meta_data FROM tokens .*` + findTokenSql = `SELECT auth_token, refresh_token, token_expiry, disconnected, auth_type, meta_data, user_guid FROM tokens .*` findUAATokenSql = `SELECT auth_token, refresh_token, token_expiry FROM tokens WHERE token_type = 'uaa' AND .*` deleteFromTokensSql = `DELETE FROM tokens` ) @@ -343,8 +343,8 @@ func TestFindCNSITokens(t *testing.T) { }) Convey("should fail to decrypt with invalid encryptionKey", func() { - rs := sqlmock.NewRows([]string{"auth_token", "refresh_token", "token_expiry", "disconnected", "auth_type", "meta_data"}). - AddRow(mockUAAToken, mockUAAToken, mockTokenExpiry, false, "oauth", "") + rs := sqlmock.NewRows([]string{"auth_token", "refresh_token", "token_expiry", "disconnected", "auth_type", "meta_data", "user_guid"}). + AddRow(mockUAAToken, mockUAAToken, mockTokenExpiry, false, "oauth", "", mockUserGuid) mock.ExpectQuery(findTokenSql). WillReturnRows(rs) @@ -355,8 +355,8 @@ func TestFindCNSITokens(t *testing.T) { }) Convey("Success case", func() { - rs := sqlmock.NewRows([]string{"auth_token", "refresh_token", "token_expiry", "disconnected", "auth_type", "meta_data"}). - AddRow(mockUAAToken, mockUAAToken, mockTokenExpiry, false, "oauth", "") + rs := sqlmock.NewRows([]string{"auth_token", "refresh_token", "token_expiry", "disconnected", "auth_type", "meta_data", "user_guid"}). + AddRow(mockUAAToken, mockUAAToken, mockTokenExpiry, false, "oauth", "", mockUserGuid) mock.ExpectQuery(findTokenSql). WillReturnRows(rs) diff --git a/src/backend/app-core/repository/tokens/tokens.go b/src/backend/app-core/repository/tokens/tokens.go index e9781c83fd..0d97161d29 100644 --- a/src/backend/app-core/repository/tokens/tokens.go +++ b/src/backend/app-core/repository/tokens/tokens.go @@ -9,6 +9,8 @@ type Token struct { Record interfaces.TokenRecord } +const SystemSharedUserGuid = "00000000-1111-2222-3333-444444444444" // User ID for the system shared user for endpoints + // Repository is an application of the repository pattern for storing tokens type Repository interface { FindAuthToken(userGUID string, encryptionKey []byte) (interfaces.TokenRecord, error) diff --git a/src/backend/cloudfoundry/main.go b/src/backend/cloudfoundry/main.go index 4c9eae7085..338f246710 100644 --- a/src/backend/cloudfoundry/main.go +++ b/src/backend/cloudfoundry/main.go @@ -133,7 +133,7 @@ func (c *CloudFoundrySpecification) cfLoginHook(context echo.Context) error { log.Infof("No, user should not auto-connect to auto-registered cloud foundry %s (previsouly disoconnected). ", cfAPI) } else { log.Infof("Yes, user should auto-connect to auto-registered cloud foundry %s.", cfAPI) - _, err := c.portalProxy.DoLoginToCNSI(context, cfCnsi.GUID) + _, err := c.portalProxy.DoLoginToCNSI(context, cfCnsi.GUID, false) return err } diff --git a/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.html b/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.html index cf090e00f0..53e4802fd5 100644 --- a/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.html +++ b/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.html @@ -22,6 +22,7 @@

+ Share this endpoint connection with other users diff --git a/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.scss b/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.scss index a3b0066713..aa87805676 100644 --- a/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.scss +++ b/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.scss @@ -22,6 +22,9 @@ display: flex; flex-direction: column; } + &__shared { + padding-bottom: 24px; + } } app-connect-endpoint-dialog { diff --git a/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.ts b/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.ts index 25433be37e..372e48a938 100644 --- a/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.ts +++ b/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.ts @@ -1,10 +1,8 @@ - -import { combineLatest as observableCombineLatest, of as observableOf, Observable, Subscription } from 'rxjs'; import { Component, Inject, OnDestroy } from '@angular/core'; -import { FormBuilder, Validators, FormGroup } from '@angular/forms'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef, MatSnackBar } from '@angular/material'; + import { Store } from '@ngrx/store'; -import { filter, map, pairwise, switchMap, delay, startWith } from 'rxjs/operators'; import { ConnectEndpoint } from '../../../store/actions/endpoint.actions'; import { ShowSnackBar } from '../../../store/actions/snackBar.actions'; @@ -15,6 +13,11 @@ import { SystemEffects } from '../../../store/effects/system.effects'; import { ActionState } from '../../../store/reducers/api-request-reducer/types'; import { selectEntity, selectRequestInfo, selectUpdateInfo } from '../../../store/selectors/api.selectors'; import { EndpointModel, endpointStoreNames, EndpointType } from '../../../store/types/endpoint.types'; +import { getCanShareTokenForEndpointType } from '../endpoint-helpers'; + +import { delay, filter, map, pairwise, startWith, switchMap } from 'rxjs/operators'; +import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; + @Component({ selector: 'app-connect-endpoint-dialog', @@ -54,6 +57,8 @@ export class ConnectEndpointDialogComponent implements OnDestroy { private hasAttemptedConnect: boolean; private authTypesForEndpoint = []; + private canShareEndpointToken = false; + // We need a delay to ensure the BE has finished registering the endpoint. // If we don't do this and if we're quick enough, we can navigate to the application page // and end up with an empty list where we should have results. @@ -77,11 +82,15 @@ export class ConnectEndpointDialogComponent implements OnDestroy { } }); + // Not all endpoint types might allow token sharing - typically types like metrics do + this.canShareEndpointToken = getCanShareTokenForEndpointType(data.type); + // Create the endpoint form const autoSelected = (this.authTypesForEndpoint.length > 0) ? this.authTypesForEndpoint[0] : {}; this.endpointForm = this.fb.group({ authType: [autoSelected.value || '', Validators.required], - authValues: this.fb.group(autoSelected.form || {}) + authValues: this.fb.group(autoSelected.form || {}), + systemShared: false }); this.setupObservables(); @@ -191,12 +200,13 @@ export class ConnectEndpointDialogComponent implements OnDestroy { submit(event) { this.hasAttemptedConnect = true; - const { guid, authType, authValues } = this.endpointForm.value; + const { guid, authType, authValues, systemShared } = this.endpointForm.value; this.store.dispatch(new ConnectEndpoint( this.data.guid, this.data.type, authType, authValues, + systemShared, this.bodyContent, )); } diff --git a/src/frontend/app/features/endpoints/endpoint-helpers.ts b/src/frontend/app/features/endpoints/endpoint-helpers.ts index 99720128a9..0a3f58b46b 100644 --- a/src/frontend/app/features/endpoints/endpoint-helpers.ts +++ b/src/frontend/app/features/endpoints/endpoint-helpers.ts @@ -11,6 +11,7 @@ export interface EndpointTypeHelper { value: EndpointType; label: string; urlValidation?: string; + allowTokenSharing?: boolean; } const endpointTypes: EndpointTypeHelper[] = [ @@ -21,7 +22,8 @@ const endpointTypes: EndpointTypeHelper[] = [ }, { value: 'metrics', - label: 'Metrics' + label: 'Metrics', + allowTokenSharing: true }, ]; @@ -36,6 +38,10 @@ export function getNameForEndpointType(type: string): string { return endpointTypesMap[type] ? endpointTypesMap[type].label : 'Unknown'; } +export function getCanShareTokenForEndpointType(type: string): boolean { + return endpointTypesMap[type] ? !!endpointTypesMap[type].allowTokenSharing : false; +} + export function getEndpointTypes() { return endpointTypes; } diff --git a/src/frontend/app/shared/components/list/list-types/cf-endpoints/cf-endpoint-card/endpoint-card.component.spec.ts b/src/frontend/app/shared/components/list/list-types/cf-endpoints/cf-endpoint-card/endpoint-card.component.spec.ts index c636ae5a6c..dedd8c71d0 100644 --- a/src/frontend/app/shared/components/list/list-types/cf-endpoints/cf-endpoint-card/endpoint-card.component.spec.ts +++ b/src/frontend/app/shared/components/list/list-types/cf-endpoints/cf-endpoint-card/endpoint-card.component.spec.ts @@ -1,8 +1,8 @@ -import { RouterTestingModule } from '@angular/router/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; -import { CfEndpointCardComponent } from './endpoint-card.component'; import { SharedModule } from '../../../../../shared.module'; +import { CfEndpointCardComponent } from './endpoint-card.component'; import { EntityMonitorFactory } from '../../../../../monitors/entity-monitor.factory.service'; import { createBasicStoreModule } from '../../../../../../test-framework/store-test-helper'; import { BaseTestModules } from '../../../../../../test-framework/cloud-foundry-endpoint-service.helper'; @@ -38,7 +38,8 @@ describe('EndpointCardComponent', () => { name: '', guid: '', }, - metricsAvailable: false + metricsAvailable: false, + system_shared_token: false, }; fixture.detectChanges(); }); diff --git a/src/frontend/app/shared/components/list/list-types/endpoint/endpoints-data-source.ts b/src/frontend/app/shared/components/list/list-types/endpoint/endpoints-data-source.ts index e47ddff03e..6ec495cf24 100644 --- a/src/frontend/app/shared/components/list/list-types/endpoint/endpoints-data-source.ts +++ b/src/frontend/app/shared/components/list/list-types/endpoint/endpoints-data-source.ts @@ -1,5 +1,4 @@ import { Store } from '@ngrx/store'; -import { pairwise, tap } from 'rxjs/operators'; import { GetAllEndpoints } from '../../../../../store/actions/endpoint.actions'; import { GetSystemInfo } from '../../../../../store/actions/system.actions'; @@ -14,6 +13,8 @@ import { TableRowStateManager } from '../../list-table/table-row/table-row-state import { IListConfig } from '../../list.component.types'; import { ListRowSateHelper } from './endpoint-data-source.helpers'; +import { pairwise, tap } from 'rxjs/operators'; + export class EndpointsDataSource extends ListDataSource { store: Store; @@ -61,6 +62,7 @@ export class EndpointsDataSource extends ListDataSource { getRowUniqueId: object => object.guid, getEmptyType: () => ({ name: '', + system_shared_token: false, metricsAvailable: false }), paginationKey: GetAllEndpoints.storeKey, diff --git a/src/frontend/app/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts b/src/frontend/app/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts index d90b566ac4..52b0aa84ea 100644 --- a/src/frontend/app/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts +++ b/src/frontend/app/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts @@ -28,7 +28,7 @@ import { TableCellEndpointNameComponent } from './table-cell-endpoint-name/table import { TableCellEndpointStatusComponent } from './table-cell-endpoint-status/table-cell-endpoint-status.component'; import { map, pairwise } from 'rxjs/operators'; -import { Observable } from 'rxjs'; +import { combineLatest, Observable } from 'rxjs'; function getEndpointTypeString(endpoint: EndpointModel): string { @@ -126,7 +126,15 @@ export class EndpointsListConfigService implements IListConfig { }, label: 'Disconnect', description: ``, // Description depends on console user permission - createVisible: (row$: Observable) => row$.pipe(map(row => row.connectionStatus === 'connected')) + createVisible: (row$: Observable) => combineLatest( + this.currentUserPermissionsService.can(CurrentUserPermissions.ENDPOINT_REGISTER), + row$ + ).pipe( + map(([isAdmin, row]) => { + const isConnected = row.connectionStatus === 'connected'; + return isConnected && (!row.system_shared_token || row.system_shared_token && isAdmin); + }) + ) }; private listActionConnect: IListAction = { diff --git a/src/frontend/app/shared/components/list/list-types/endpoint/table-cell-endpoint-name/table-cell-endpoint-name.component.html b/src/frontend/app/shared/components/list/list-types/endpoint/table-cell-endpoint-name/table-cell-endpoint-name.component.html index 479d246726..7bac8d5932 100644 --- a/src/frontend/app/shared/components/list/list-types/endpoint/table-cell-endpoint-name/table-cell-endpoint-name.component.html +++ b/src/frontend/app/shared/components/list/list-types/endpoint/table-cell-endpoint-name/table-cell-endpoint-name.component.html @@ -1,2 +1,5 @@ -{{ row.name }} -{{ row.name }} \ No newline at end of file +
+ {{ row.name }} + {{ row.name }} + share +
\ No newline at end of file diff --git a/src/frontend/app/shared/components/list/list-types/endpoint/table-cell-endpoint-name/table-cell-endpoint-name.component.scss b/src/frontend/app/shared/components/list/list-types/endpoint/table-cell-endpoint-name/table-cell-endpoint-name.component.scss index e69de29bb2..0c52fc94ed 100644 --- a/src/frontend/app/shared/components/list/list-types/endpoint/table-cell-endpoint-name/table-cell-endpoint-name.component.scss +++ b/src/frontend/app/shared/components/list/list-types/endpoint/table-cell-endpoint-name/table-cell-endpoint-name.component.scss @@ -0,0 +1,10 @@ +.endpoint-name-cell { + align-items: center; + display: flex; + &__icon { + font-size: 20px; + height: 20px; + padding-left: 8px; + width: 20px; + } +} diff --git a/src/frontend/app/shared/components/list/list-types/endpoint/table-cell-endpoint-name/table-cell-endpoint-name.component.ts b/src/frontend/app/shared/components/list/list-types/endpoint/table-cell-endpoint-name/table-cell-endpoint-name.component.ts index 9acbb62761..47fcad065b 100644 --- a/src/frontend/app/shared/components/list/list-types/endpoint/table-cell-endpoint-name/table-cell-endpoint-name.component.ts +++ b/src/frontend/app/shared/components/list/list-types/endpoint/table-cell-endpoint-name/table-cell-endpoint-name.component.ts @@ -1,4 +1,5 @@ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; + import { TableCellCustom } from '../../../list.types'; @Component({ diff --git a/src/frontend/app/store/actions/endpoint.actions.ts b/src/frontend/app/store/actions/endpoint.actions.ts index 2672058f68..83be4e7c7e 100644 --- a/src/frontend/app/store/actions/endpoint.actions.ts +++ b/src/frontend/app/store/actions/endpoint.actions.ts @@ -1,8 +1,8 @@ import { Action } from '@ngrx/store'; import { endpointSchemaKey } from '../helpers/entity-factory'; +import { EndpointModel, EndpointType, INewlyConnectedEndpointInfo } from '../types/endpoint.types'; import { PaginatedAction } from '../types/pagination.types'; -import { EndpointType, EndpointModel, INewlyConnectedEndpointInfo } from '../types/endpoint.types'; export const GET_ENDPOINTS = '[Endpoints] Get all'; export const GET_ENDPOINTS_START = '[Endpoints] Get all start'; @@ -88,6 +88,7 @@ export class ConnectEndpoint extends EndpointAction { public endpointType: EndpointType, public authType: string, public authValues: AuthParams, + public systemShared: boolean, public body: string, ) { super(); diff --git a/src/frontend/app/store/effects/endpoint.effects.ts b/src/frontend/app/store/effects/endpoint.effects.ts index 1197418943..3ffcaad9cf 100644 --- a/src/frontend/app/store/effects/endpoint.effects.ts +++ b/src/frontend/app/store/effects/endpoint.effects.ts @@ -2,6 +2,7 @@ import {catchError, mergeMap } from 'rxjs/operators'; import { HttpClient, HttpHeaders, HttpParams, HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; + import { Actions, Effect } from '@ngrx/effects'; import { Store } from '@ngrx/store'; @@ -94,6 +95,7 @@ export class EndpointsEffect { ...action.authValues, 'cnsi_guid': action.guid, 'connect_type': action.authType, + 'system_shared': action.systemShared, }, // Fix for #angular/18261 encoder: new BrowserStandardEncoder() diff --git a/src/frontend/app/store/reducers/current-user-roles-reducer/current-user-role-session.reducer.ts b/src/frontend/app/store/reducers/current-user-roles-reducer/current-user-role-session.reducer.ts index 5ef2db30eb..8c8ab4b752 100644 --- a/src/frontend/app/store/reducers/current-user-roles-reducer/current-user-role-session.reducer.ts +++ b/src/frontend/app/store/reducers/current-user-roles-reducer/current-user-role-session.reducer.ts @@ -1,16 +1,15 @@ +import { ScopeStrings } from '../../../core/current-user-permissions.config'; import { VerifiedSession } from '../../actions/auth.actions'; +import { EndpointActionComplete } from '../../actions/endpoint.actions'; +import { SessionUser } from '../../types/auth.types'; import { - ICurrentUserRolesState, + getDefaultEndpointRoles, IAllCfRolesState, ICfRolesState, - getDefaultEndpointRoles, - IStratosRolesState, - IGlobalRolesState + ICurrentUserRolesState, + IGlobalRolesState, } from '../../types/current-user-roles.types'; -import { SessionData, SessionDataEndpoint, SessionEndpoints, SessionUser, SessionEndpoint } from '../../types/auth.types'; -import { ScopeStrings } from '../../../core/current-user-permissions.config'; -import { EndpointActionComplete } from '../../actions/endpoint.actions'; -import { EndpointModel, INewlyConnectedEndpointInfo, EndpointUser } from '../../types/endpoint.types'; +import { EndpointUser, INewlyConnectedEndpointInfo } from '../../types/endpoint.types'; interface PartialEndpoint { user: EndpointUser | SessionUser; diff --git a/src/frontend/app/store/types/endpoint.types.ts b/src/frontend/app/store/types/endpoint.types.ts index c91532421f..656dbf4d3d 100644 --- a/src/frontend/app/store/types/endpoint.types.ts +++ b/src/frontend/app/store/types/endpoint.types.ts @@ -1,6 +1,6 @@ -import { RequestSectionKeys, TRequestTypeKeys } from '../reducers/api-request-reducer/types'; -import { endpointSchemaKey } from '../helpers/entity-factory'; import { ScopeStrings } from '../../core/current-user-permissions.config'; +import { endpointSchemaKey } from '../helpers/entity-factory'; +import { RequestSectionKeys, TRequestTypeKeys } from '../reducers/api-request-reducer/types'; export interface INewlyConnectedEndpointInfo { account: string; @@ -43,6 +43,7 @@ export interface EndpointModel { metadata?: { metrics: string }; + system_shared_token: boolean; // These are generated client side when we login registered?: boolean; connectionStatus?: endpointConnectionStatus; diff --git a/src/frontend/app/test-framework/store-test-helper.ts b/src/frontend/app/test-framework/store-test-helper.ts index 5cdced67f7..c34a1b1c03 100644 --- a/src/frontend/app/test-framework/store-test-helper.ts +++ b/src/frontend/app/test-framework/store-test-helper.ts @@ -1,4 +1,5 @@ import { ModuleWithProviders } from '@angular/core'; + import { StoreModule } from '@ngrx/store'; import { AppState } from '../store/app-state'; @@ -21733,6 +21734,7 @@ const testInitialStoreState: AppState = { }, connectionStatus: 'connected', registered: true, + system_shared_token: false, metricsAvailable: false }, },