From b701d21d8987a282f2f864d2aa997dbcbea0594c Mon Sep 17 00:00:00 2001 From: DenisSinelnikov <142215442+DenisSinelnikov@users.noreply.github.com> Date: Thu, 25 Apr 2024 17:20:24 +0400 Subject: [PATCH] Cb 4268 force logout (#2549) * CB-4703. Hide private ssl key, added new parameter to features * CB-4631. Fix Build * CB-4703. Hide private ssl key, added new parameter to features * CB-4631. Fix Build * CB-4631. Added logic * CB-4728. Refactor after review * CB-4268: adds too many session checkbox for local and federated authentication * CB-4268 ux improve for force session dialog. do not close main popup if confirm dialog rejected * CB-4268 cleanup * CB-4268 adds too-many-sessions-plugin to close error of the external window while logging in * CB-4268 cleanup * CB-4268 cleanup * CB-4268 auth dialog: removes unneeded state * CB-4268. Add logic to force logout for federated users * CB-4268. Added redirect for Federated users * CB-4268. Update script * CB-4268 split force session dialog checkbox for each tab its own instance * CB-4268 set exceptions for each auth tab individually * CB-4268 show error for all auth tabs * CB-4268. Update script, refactor event for user * CB-4268. Fixed force logout for sso * CB-4268. Fixed class cast exception * CB-4268. Refactor after review * Revert "CB-4268 show error for all auth tabs" This reverts commit 996bdfebdbcf7449474ba1b61c24fb9e211d698b. * Revert "CB-4268 set exceptions for each auth tab individually" This reverts commit dbeca03a3142aef2140fc8eb9bfe381b3b4b3503. * Revert "CB-4268 split force session dialog checkbox for each tab its own instance" This reverts commit cf79208d41914b3e4dba10e272e8582a1f9f5b69. * CB-4268 show error for all auth dialog tabs * CB-4268 resets error state on auth dialog tab change * CB-4268 fix: reset error state only on tab change * CB-4268 auth login function cleanup * CB-4268. Refactor after review, cleanup code * CB-4268 removes too-many-sessions plugin * CB-4268 removes unneeded translations * CB-4268 fix: show login button only for non federated auth tabs * CB-4268 refactor: reset error on switching auth mode * CB-4268 stop resetting error with flag for auth dialog * CB-4268 stop resetting error with flag for auth dialog (reset error default true) * CB-4268 rename to ssoError.html * CB-4268. Rename parameter for html * CB-4268. Revert * CB-4854. Refactor after review * CB-4854. Refactor after review * CB-4268 adds tooltip for force logout checkbox * CB-4268 changes force sessions popup title to shorter version --------- Co-authored-by: sergeyteleshev Co-authored-by: Evgenia Bezborodova <139753579+EvgeniaBzzz@users.noreply.github.com> --- .../io/cloudbeaver/auth/CBAuthConstants.java | 1 + .../model/session/BaseWebSession.java | 8 + .../cloudbeaver/model/session/WebSession.java | 19 +++ .../bundles/io.cloudbeaver.server/plugin.xml | 2 +- .../events/WSUserDeletedEventHandler.java | 29 ---- .../server/events/WSUserEventHandler.java | 50 +++++++ .../server/servlets/CBStaticServlet.java | 3 +- .../service/session/WebSessionManager.java | 22 ++- .../schema/service.auth.graphqls | 3 +- .../service/auth/DBWServiceAuth.java | 3 +- .../service/auth/RPSessionHandler.java | 2 +- .../service/auth/WebServiceBindingAuth.java | 4 +- .../service/auth/impl/WebServiceAuthImpl.java | 15 +- .../db/cb_schema_create.sql | 2 + .../db/cb_schema_update_19.sql | 5 + .../CBEmbeddedSecurityController.java | 140 ++++++++++++++---- .../service/security/SMActiveSession.java | 24 +++ .../service/security/db/CBDatabase.java | 2 +- .../src/UserInfoResource.ts | 4 +- .../src/HTMLTemplates/ssoError.html.ejs | 56 +++++++ .../configs/webpack.product.config.js | 16 ++ .../configs/webpack.product.dev.config.js | 9 ++ .../packages/core-sdk/src/EServerErrorCode.ts | 1 + .../src/queries/authentication/authLogin.gql | 12 +- .../src/Dialog/AuthDialog.m.css | 10 ++ .../src/Dialog/AuthDialog.tsx | 33 ++++- .../src/Dialog/AuthDialogFooter.tsx | 15 +- .../src/Dialog/useAuthDialogState.ts | 70 +++++++-- .../plugin-authentication/src/locales/en.ts | 11 ++ .../plugin-authentication/src/locales/it.ts | 11 ++ .../plugin-authentication/src/locales/ru.ts | 11 ++ .../plugin-authentication/src/locales/zh.ts | 11 ++ .../packages/plugin-data-import/package.json | 6 - .../packages/plugin-data-import/tsconfig.json | 18 --- .../packages/plugin-sso/src/ssoError.html.ejs | 11 ++ .../plugin-user-profile-settings/package.json | 1 - 36 files changed, 511 insertions(+), 129 deletions(-) delete mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserDeletedEventHandler.java create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserEventHandler.java create mode 100644 server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_19.sql create mode 100644 server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/SMActiveSession.java create mode 100644 webapp/packages/core-blocks/src/HTMLTemplates/ssoError.html.ejs create mode 100644 webapp/packages/plugin-sso/src/ssoError.html.ejs diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/CBAuthConstants.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/CBAuthConstants.java index 64156248db..24769da64f 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/CBAuthConstants.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/CBAuthConstants.java @@ -25,6 +25,7 @@ public interface CBAuthConstants { String CB_AUTH_ID_REQUEST_PARAM = "authId"; String CB_AUTO_LOGIN_REQUEST_PARAM = "autoLogin"; String CB_REDIRECT_URL_REQUEST_PARAM = "redirectUrl"; + String CB_FORCE_SESSIONS_LOGOUT = "forceSessionsLogout"; // Default max idle time (10 minutes) long MAX_SESSION_IDLE_TIME = 10 * 60 * 1000; diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java index 86c8fd33c7..1fddbd605a 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java @@ -173,6 +173,14 @@ public abstract class BaseWebSession extends AbstractSessionPersistent { @Override public void close() { super.close(); + cleanUpSession(); + } + + public void close(boolean clearTokens) { + cleanUpSession(); + } + + private void cleanUpSession() { var sessionExpiredEvent = new WSSessionExpiredEvent(); application.getEventController().addEvent(new WSEventDeleteTempFile(getSessionId())); synchronized (sessionEventHandlers) { diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java index e3279e88bb..ad556f4038 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java @@ -644,6 +644,25 @@ public class WebSession extends BaseWebSession super.close(); } + @Override + public void close(boolean clearTokens) { + try { + resetNavigationModel(); + resetSessionCache(); + } catch (Throwable e) { + log.error(e); + } + if (clearTokens) { + try { + clearAuthTokens(); + } catch (Exception e) { + log.error("Error closing web session tokens"); + } + } + this.userContext.setUser(null); + super.close(clearTokens); + } + private List clearAuthTokens() throws DBException { ArrayList tokensCopy; synchronized (authTokens) { diff --git a/server/bundles/io.cloudbeaver.server/plugin.xml b/server/bundles/io.cloudbeaver.server/plugin.xml index 69c1282639..73656b40b7 100644 --- a/server/bundles/io.cloudbeaver.server/plugin.xml +++ b/server/bundles/io.cloudbeaver.server/plugin.xml @@ -70,7 +70,7 @@ - + diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserDeletedEventHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserDeletedEventHandler.java deleted file mode 100644 index fa31cdcfbf..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserDeletedEventHandler.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.cloudbeaver.server.events; - -import io.cloudbeaver.server.CBPlatform; -import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.model.websocket.WSEventHandler; -import org.jkiss.dbeaver.model.websocket.event.WSUserDeletedEvent; - -public class WSUserDeletedEventHandler implements WSEventHandler { - @Override - public void handleEvent(@NotNull EVENT event) { - CBPlatform.getInstance().getSessionManager().closeUserSessions(event.getDeletedUserId()); - } -} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserEventHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserEventHandler.java new file mode 100644 index 0000000000..edb0a97d26 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserEventHandler.java @@ -0,0 +1,50 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.events; + +import io.cloudbeaver.server.CBPlatform; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.websocket.WSEventHandler; +import org.jkiss.dbeaver.model.websocket.event.WSAbstractEvent; +import org.jkiss.dbeaver.model.websocket.event.WSEventType; +import org.jkiss.dbeaver.model.websocket.event.WSUserCloseSessionsEvent; +import org.jkiss.dbeaver.model.websocket.event.WSUserDeletedEvent; + +public class WSUserEventHandler implements WSEventHandler { + @Override + public void handleEvent(@NotNull EVENT event) { + var eventType = WSEventType.valueById(event.getId()); + if (eventType == null) { + return; + } + switch (eventType) { + case CLOSE_USER_SESSIONS: + if (event instanceof WSUserCloseSessionsEvent closeSessionsEvent) { + CBPlatform.getInstance().getSessionManager().closeSessions(closeSessionsEvent.getSessionIds()); + } + break; + case USER_DELETED: + if (event instanceof WSUserDeletedEvent userDeletedEvent) { + CBPlatform.getInstance().getSessionManager().closeUserSession(userDeletedEvent.getUserId()); + } + break; + default: + break; + } + + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java index ac9d45bd16..11ad45034e 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java @@ -159,7 +159,8 @@ public class CBStaticServlet extends DefaultServlet { WebSession.CB_SESSION_TYPE, authProvider.getId(), activeAuthConfig.getId(), - Map.of() + Map.of(), + false ); String signInLink = authInfo.getRedirectUrl(); //ignore current routing if non-root page is open diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java index d77bf2a154..4941d8697e 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java @@ -346,13 +346,27 @@ public class WebSessionManager { } } - public void closeUserSessions(@NotNull String userId) { + public void closeUserSession(@NotNull String userId) { synchronized (sessionMap) { for (Iterator iterator = sessionMap.values().iterator(); iterator.hasNext(); ) { var session = iterator.next(); - if (CommonUtils.equalObjects(session.getUserContext().getUserId(), userId)) { - iterator.remove(); - session.close(); + if (CommonUtils.equalObjects(session.getUserContext().getUserId(), userId)) { + iterator.remove(); + session.close(); + } + } + } + } + + public void closeSessions(@NotNull List smSessionsId) { + synchronized (sessionMap) { + for (Iterator iterator = sessionMap.values().iterator(); iterator.hasNext(); ) { + var session = iterator.next(); + for (String smSessionId : smSessionsId) { + if (CommonUtils.equalObjects(session.getUserContext().getSmSessionId(), smSessionId)) { + iterator.remove(); + session.close(false); + } } } } diff --git a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls index 74e2319cb0..6ffefdf0f7 100644 --- a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls +++ b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls @@ -140,7 +140,8 @@ type UserInfo { extend type Query { # Authorize user using specified auth provider. If linkUser=true then associates new - authLogin(provider: ID!, configuration: ID, credentials: Object, linkUser: Boolean): AuthInfo! + # If forceSessionsLogout=true then kill another sessions + authLogin(provider: ID!, configuration: ID, credentials: Object, linkUser: Boolean, forceSessionsLogout: Boolean): AuthInfo! authUpdateStatus(authId: ID!, linkUser: Boolean): AuthInfo! diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java index 54c1e0efab..ee691566de 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java @@ -38,7 +38,8 @@ public interface DBWServiceAuth extends DBWService { @NotNull String providerId, @Nullable String providerConfigurationId, @Nullable Map credentials, - boolean linkWithActiveUser) throws DBWebException; + boolean linkWithActiveUser, + boolean forceSessionsLogout) throws DBWebException; @WebAction(authRequired = false) diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java index 020d370df0..e2bb777865 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java @@ -127,7 +127,7 @@ public class RPSessionHandler implements DBWSessionHandler { webSession.getSessionId(), currentSmSessionId, sessionParameters, - WebSession.CB_SESSION_TYPE, authProvider.getId(), configuration.getId(), userCredentials); + WebSession.CB_SESSION_TYPE, authProvider.getId(), configuration.getId(), userCredentials, false); new WebSessionAuthProcessor(webSession, smAuthInfo, false).authenticateSession(); log.debug(MessageFormat.format( "Successful reverse proxy authentication: user ''{0}'' with teams {1}", userName, userTeams)); diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java index c480e48380..1d54b4466a 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java @@ -41,7 +41,9 @@ public class WebServiceBindingAuth extends WebServiceBindingBase env.getArgument("provider"), env.getArgument("configuration"), env.getArgument("credentials"), - CommonUtils.toBoolean(env.getArgument("linkUser")))) + CommonUtils.toBoolean(env.getArgument("linkUser")), + CommonUtils.toBoolean(env.getArgument("forceSessionsLogout")) + )) .dataFetcher("authLogoutExtended", env -> getService(env).authLogout( getWebSession(env, false), env.getArgument("provider"), diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java index 53c4821993..ca7466b619 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java @@ -45,6 +45,7 @@ import org.jkiss.dbeaver.model.auth.SMSessionExternal; import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor; import org.jkiss.dbeaver.model.security.SMController; import org.jkiss.dbeaver.model.security.SMSubjectType; +import org.jkiss.dbeaver.model.security.exception.SMTooManySessionsException; import org.jkiss.dbeaver.model.security.user.SMUser; import org.jkiss.utils.CommonUtils; @@ -67,7 +68,8 @@ public class WebServiceAuthImpl implements DBWServiceAuth { @NotNull String providerId, @Nullable String providerConfigurationId, @Nullable Map authParameters, - boolean linkWithActiveUser + boolean linkWithActiveUser, + boolean forceSessionsLogout ) throws DBWebException { if (CommonUtils.isEmpty(providerId)) { throw new DBWebException("Missing auth provider parameter"); @@ -93,7 +95,8 @@ public class WebServiceAuthImpl implements DBWServiceAuth { WebSession.CB_SESSION_TYPE, providerId, providerConfigurationId, - authParameters + authParameters, + forceSessionsLogout ); linkWithActiveUser = linkWithActiveUser && CBApplication.getInstance().getAppConfiguration().isLinkExternalCredentialsWithUser(); @@ -105,6 +108,8 @@ public class WebServiceAuthImpl implements DBWServiceAuth { var authProcessor = new WebSessionAuthProcessor(webSession, smAuthInfo, linkWithActiveUser); return new WebAuthStatus(smAuthInfo.getAuthStatus(), authProcessor.authenticateSession()); } + } catch (SMTooManySessionsException e) { + throw new DBWebException("User authentication failed", e.getErrorType(), e); } catch (Exception e) { throw new DBWebException("User authentication failed", e); } @@ -123,12 +128,16 @@ public class WebServiceAuthImpl implements DBWServiceAuth { case IN_PROGRESS: return new WebAuthStatus(smAuthInfo.getAuthAttemptId(), smAuthInfo.getRedirectUrl(), smAuthInfo.getAuthStatus()); case ERROR: - throw new DBWebException(smAuthInfo.getError()); + throw new DBWebException(smAuthInfo.getError(), smAuthInfo.getErrorCode()); case EXPIRED: throw new DBException("Authorization has already been processed"); default: throw new DBWebException("Unknown auth status:" + smAuthInfo.getAuthStatus()); } + } catch (DBWebException e) { + throw e; + } catch (SMTooManySessionsException e) { + throw new DBWebException(e.getMessage(), e.getErrorType()); } catch (DBException e) { throw new DBWebException(e.getMessage(), e); } diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql index f0cde846a5..af81539fcf 100644 --- a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql @@ -213,6 +213,8 @@ CREATE TABLE {table_prefix}CB_AUTH_ATTEMPT APP_SESSION_STATE TEXT NOT NULL, IS_MAIN_AUTH CHAR(1) DEFAULT 'Y' NOT NULL, AUTH_USERNAME VARCHAR(128) NULL, + ERROR_CODE VARCHAR(128) NULL, + FORCE_SESSION_LOGOUT CHAR(1) DEFAULT 'N' NOT NULL, CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY (AUTH_ID), diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_19.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_19.sql new file mode 100644 index 0000000000..5639d46483 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_19.sql @@ -0,0 +1,5 @@ +ALTER TABLE {table_prefix}CB_AUTH_ATTEMPT + ADD ERROR_CODE VARCHAR(128) NULL; + +ALTER TABLE {table_prefix}CB_AUTH_ATTEMPT + ADD FORCE_SESSION_LOGOUT CHAR(1) DEFAULT 'N' NOT NULL; \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java index ed5d7b6a13..6531f6eaad 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java @@ -49,6 +49,8 @@ import org.jkiss.dbeaver.model.security.exception.SMException; import org.jkiss.dbeaver.model.security.exception.SMRefreshTokenExpiredException; import org.jkiss.dbeaver.model.security.user.*; import org.jkiss.dbeaver.model.sql.SQLUtils; +import org.jkiss.dbeaver.model.websocket.event.WSEventType; +import org.jkiss.dbeaver.model.websocket.event.WSUserCloseSessionsEvent; import org.jkiss.dbeaver.model.websocket.event.WSUserDeletedEvent; import org.jkiss.dbeaver.model.websocket.event.permissions.WSObjectPermissionEvent; import org.jkiss.dbeaver.model.websocket.event.permissions.WSSubjectPermissionEvent; @@ -206,7 +208,7 @@ public class CBEmbeddedSecurityController } catch (SQLException e) { throw new DBCException("Error deleting user from database", e); } - var event = new WSUserDeletedEvent(userId); + var event = new WSUserDeletedEvent(WSEventType.USER_DELETED, userId); application.getEventController().addEvent(event); } @@ -1342,7 +1344,8 @@ public class CBEmbeddedSecurityController @NotNull SMSessionType sessionType, @NotNull String authProviderId, @Nullable String authProviderConfigurationId, - @NotNull Map userCredentials + @NotNull Map userCredentials, + boolean forceSessionsLogout ) throws DBException { if (isProviderDisabled(authProviderId, authProviderConfigurationId)) { throw new SMException("Unsupported authentication provider: " + authProviderId); @@ -1382,7 +1385,9 @@ public class CBEmbeddedSecurityController previousSmSessionId, sessionType, sessionParameters, - isMainSession + isMainSession, + null, + forceSessionsLogout ); throw e; } @@ -1397,7 +1402,9 @@ public class CBEmbeddedSecurityController previousSmSessionId, sessionType, sessionParameters, - isMainSession + isMainSession, + null, + forceSessionsLogout ); if (SMAuthProviderFederated.class.isAssignableFrom(authProviderInstance.getClass())) { @@ -1409,7 +1416,7 @@ public class CBEmbeddedSecurityController providerConfig.getParameters()); Map authData = Map.of(new SMAuthConfigurationReference(authProviderId, authProviderConfigurationId), filteredUserCreds); - return SMAuthInfo.inProgress(authAttemptId, signInLink, signOutLink, authData, isMainSession); + return SMAuthInfo.inProgress(authAttemptId, signInLink, signOutLink, authData, isMainSession, forceSessionsLogout); } txn.commit(); return finishAuthentication( @@ -1419,9 +1426,11 @@ public class CBEmbeddedSecurityController null, Map.of(new SMAuthConfigurationReference(authProviderId, authProviderConfigurationId), securedUserIdentifyingCredentials), - isMainSession + isMainSession, + forceSessionsLogout ), - true + true, + forceSessionsLogout ); } } catch (SQLException e) { @@ -1453,7 +1462,9 @@ public class CBEmbeddedSecurityController String prevSessionId, SMSessionType sessionType, Map sessionParameters, - boolean isMainSession + boolean isMainSession, + @Nullable String errorCode, + boolean forceSessionsLogout ) throws DBException { String authAttemptId = UUID.randomUUID().toString(); try (Connection dbCon = database.openConnection()) { @@ -1469,8 +1480,8 @@ public class CBEmbeddedSecurityController database.normalizeTableNames( "INSERT INTO {table_prefix}CB_AUTH_ATTEMPT" + "(AUTH_ID,AUTH_STATUS,APP_SESSION_ID,SESSION_TYPE,APP_SESSION_STATE," + - "SESSION_ID,IS_MAIN_AUTH, AUTH_USERNAME) " + - "VALUES(?,?,?,?,?,?,?,?)" + "SESSION_ID,IS_MAIN_AUTH,AUTH_USERNAME,ERROR_CODE,FORCE_SESSION_LOGOUT) " + + "VALUES(?,?,?,?,?,?,?,?,?,?)" ) )) { dbStat.setString(1, authAttemptId); @@ -1494,6 +1505,8 @@ public class CBEmbeddedSecurityController } else { dbStat.setString(8, null); } + dbStat.setString(9, errorCode); + dbStat.setString(10, forceSessionsLogout ? CHAR_BOOL_TRUE : CHAR_BOOL_FALSE); dbStat.execute(); } @@ -1559,14 +1572,15 @@ public class CBEmbeddedSecurityController @NotNull String authId, @NotNull SMAuthStatus authStatus, @NotNull Map authInfo, - @Nullable String error + @Nullable String error, + @Nullable String errorCode ) throws DBException { var existAuthInfo = getAuthStatus(authId); if (existAuthInfo.getAuthStatus() != SMAuthStatus.IN_PROGRESS) { throw new SMException("Authorization already finished and cannot be updated"); } var authSessionInfo = readAuthAttemptSessionInfo(authId); - updateAuthStatus(authId, authStatus, authInfo, error, authSessionInfo.getSmSessionId()); + updateAuthStatus(authId, authStatus, authInfo, error, authSessionInfo.getSmSessionId(), errorCode); } private void updateAuthStatus( @@ -1574,16 +1588,18 @@ public class CBEmbeddedSecurityController @NotNull SMAuthStatus authStatus, @NotNull Map authInfo, @Nullable String error, - @Nullable String smSessionId + @Nullable String smSessionId, + @Nullable String errorCode ) throws DBException { try (Connection dbCon = database.openConnection(); JDBCTransaction txn = new JDBCTransaction(dbCon)) { try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames( - "UPDATE {table_prefix}CB_AUTH_ATTEMPT SET AUTH_STATUS=?,AUTH_ERROR=?,SESSION_ID=? WHERE AUTH_ID=?" + "UPDATE {table_prefix}CB_AUTH_ATTEMPT SET AUTH_STATUS=?,AUTH_ERROR=?,SESSION_ID=?,ERROR_CODE=? WHERE AUTH_ID=?" ))) { dbStat.setString(1, authStatus.toString()); JDBCUtils.setStringOrNull(dbStat, 2, error); JDBCUtils.setStringOrNull(dbStat, 3, smSessionId); - dbStat.setString(4, authId); + JDBCUtils.setStringOrNull(dbStat, 4, errorCode); + dbStat.setString(5, authId); if (dbStat.executeUpdate() <= 0) { throw new DBCException("Auth attempt '" + authId + "' doesn't exist"); } @@ -1633,7 +1649,8 @@ public class CBEmbeddedSecurityController SMAuthStatus.EXPIRED, smAuthInfo.getAuthData(), null, - smAuthInfo.getAuthPermissions().getSessionId() + smAuthInfo.getAuthPermissions().getSessionId(), + smAuthInfo.getErrorCode() ); } return smAuthInfo; @@ -1644,11 +1661,13 @@ public class CBEmbeddedSecurityController SMAuthStatus smAuthStatus; String authError; String smSessionId; + String errorCode; + boolean forceSessionsLogout; boolean isMainAuth; try (PreparedStatement dbStat = dbCon.prepareStatement( database.normalizeTableNames( - "SELECT AUTH_STATUS,AUTH_ERROR,SESSION_ID,IS_MAIN_AUTH FROM {table_prefix}CB_AUTH_ATTEMPT WHERE " + - "AUTH_ID=?" + "SELECT AUTH_STATUS,AUTH_ERROR,SESSION_ID,IS_MAIN_AUTH,ERROR_CODE,FORCE_SESSION_LOGOUT" + + " FROM {table_prefix}CB_AUTH_ATTEMPT WHERE AUTH_ID=?" ) )) { dbStat.setString(1, authId); @@ -1660,6 +1679,8 @@ public class CBEmbeddedSecurityController authError = dbResult.getString(2); smSessionId = dbResult.getString(3); isMainAuth = CHAR_BOOL_TRUE.equals(dbResult.getString(4)); + errorCode = dbResult.getString(5); + forceSessionsLogout = CHAR_BOOL_TRUE.equals(dbResult.getString(6)); } } Map authData = new LinkedHashMap<>(); @@ -1703,9 +1724,9 @@ public class CBEmbeddedSecurityController if (smAuthStatus != SMAuthStatus.SUCCESS) { switch (smAuthStatus) { case IN_PROGRESS: - return SMAuthInfo.inProgress(authId, signInLink, signOutLink, authData, isMainAuth); + return SMAuthInfo.inProgress(authId, signInLink, signOutLink, authData, isMainAuth, forceSessionsLogout); case ERROR: - return SMAuthInfo.error(authId, authError, isMainAuth); + return SMAuthInfo.error(authId, authError, isMainAuth, errorCode); case EXPIRED: return SMAuthInfo.expired(authId, readExpiredData ? authData : Map.of(), isMainAuth); default: @@ -1920,12 +1941,13 @@ public class CBEmbeddedSecurityController @Override public SMAuthInfo finishAuthentication(@NotNull String authId) throws DBException { SMAuthInfo authInfo = getAuthStatus(authId); - return finishAuthentication(authInfo, false); + return finishAuthentication(authInfo, false, authInfo.isForceSessionsLogout()); } private SMAuthInfo finishAuthentication( @NotNull SMAuthInfo authInfo, - boolean isSyncAuth + boolean isSyncAuth, + boolean forceSessionsLogout ) throws DBException { boolean isAsyncAuth = !isSyncAuth; String authId = authInfo.getAuthAttemptId(); @@ -2002,8 +2024,8 @@ public class CBEmbeddedSecurityController if (userIdFromCreds == null) { var error = "Invalid user credentials"; - updateAuthStatus(authId, SMAuthStatus.ERROR, dbStoredUserData, error); - return SMAuthInfo.error(authId, error, isMainAuthSession); + updateAuthStatus(authId, SMAuthStatus.ERROR, dbStoredUserData, error, null); + return SMAuthInfo.error(authId, error, isMainAuthSession, null); } if (autoAssign != null && !CommonUtils.isEmpty(autoAssign.getExternalTeamIds())) { @@ -2048,7 +2070,11 @@ public class CBEmbeddedSecurityController smSessionId = authAttemptSessionInfo.getSmSessionId(); } + if (forceSessionsLogout && CommonUtils.isNotEmpty(activeUserId) && isMainAuthSession) { + killAllExistsUserSessions(activeUserId); + } smTokens = generateNewSessionToken(smSessionId, activeUserId, tokenAuthRole, dbCon); + permissions = new SMAuthPermissions( activeUserId, smSessionId, getUserPermissions(activeUserId, tokenAuthRole) ); @@ -2056,12 +2082,12 @@ public class CBEmbeddedSecurityController } } catch (SQLException e) { var error = "Error during token generation"; - updateAuthStatus(authId, SMAuthStatus.ERROR, dbStoredUserData, error); + updateAuthStatus(authId, SMAuthStatus.ERROR, dbStoredUserData, error, null); throw new SMException(error, e); } } var authStatus = isSyncAuth ? SMAuthStatus.EXPIRED : SMAuthStatus.SUCCESS; - updateAuthStatus(authId, authStatus, dbStoredUserData, null, permissions.getSessionId()); + updateAuthStatus(authId, authStatus, dbStoredUserData, null, permissions.getSessionId(), null); if (isMainAuthSession) { return SMAuthInfo.successMainSession( @@ -2304,6 +2330,68 @@ public class CBEmbeddedSecurityController return generateNewSessionTokens(smSessionId, userId, authRole, dbCon); } + protected void killAllExistsUserSessions( + @NotNull String userId + ) throws SQLException, DBException { + LocalDateTime currentTime = LocalDateTime.now(); + List smSessionsId = findActiveUserSessions(userId, currentTime) + .stream().map(SMActiveSession::sessionId).collect(Collectors.toList()); + deleteSessionsTokens(smSessionsId); + application.getEventController().addEvent(new WSUserCloseSessionsEvent(smSessionsId, WSEventType.CLOSE_USER_SESSIONS)); + } + + /** + * Count active user's sm sessions + */ + @NotNull + public List findActiveUserSessions( + @NotNull String userId, + @NotNull LocalDateTime currentTime + ) throws DBException { + var activeSessions = new ArrayList(); + try (var dbCon = database.openConnection()) { + try (PreparedStatement dbStat = dbCon.prepareStatement( + database.normalizeTableNames("SELECT DISTINCT CAT.SESSION_ID, CAT.EXPIRATION_TIME " + + "FROM {table_prefix}CB_AUTH_TOKEN CAT " + + "JOIN {table_prefix}CB_AUTH_ATTEMPT CAA ON CAA.SESSION_ID = CAT.SESSION_ID WHERE " + + "CAT.USER_ID=? AND CAA.AUTH_STATUS=? AND CAT.EXPIRATION_TIME>? " + + "ORDER BY CAT.EXPIRATION_TIME" + )) + ) { + dbStat.setString(1, userId); + //count only tokens actually used by users + dbStat.setString(2, SMAuthStatus.EXPIRED.name()); + dbStat.setTimestamp(3, Timestamp.valueOf(currentTime)); + try (ResultSet dbResult = dbStat.executeQuery()) { + while (dbResult.next()) { + var sessionId = dbResult.getString(1); + var expirationTime = dbResult.getTimestamp(2); + activeSessions.add(new SMActiveSession(sessionId, expirationTime)); + } + } + return activeSessions; + } + } catch (SQLException e) { + throw new DBException("Error counting active user's session", e); + } + } + + private void deleteSessionsTokens(@NotNull List sessionsId) throws DBException { + try (var dbCon = database.openConnection()) { + try (PreparedStatement dbStat = dbCon.prepareStatement( + database.normalizeTableNames("DELETE FROM {table_prefix}CB_AUTH_TOKEN WHERE SESSION_ID = ?")) + ) { + for (String sessionId : sessionsId) { + dbStat.setString(1, sessionId); + dbStat.executeUpdate(); + } + } + } catch (SQLException e) { + throw new DBException("Error delete active user's session", e); + } + } + + private SMTokens generateNewSessionTokens( @NotNull String smSessionId, @Nullable String userId, diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/SMActiveSession.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/SMActiveSession.java new file mode 100644 index 0000000000..acc4b2884f --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/SMActiveSession.java @@ -0,0 +1,24 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp + * + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of DBeaver Corp and its suppliers, if any. + * The intellectual and technical concepts contained + * herein are proprietary to DBeaver Corp and its suppliers + * and may be covered by U.S. and Foreign Patents, + * patents in process, and are protected by trade secret or copyright law. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from DBeaver Corp. + */ +package io.cloudbeaver.service.security; + +import org.jkiss.code.NotNull; + +import java.sql.Timestamp; + +public record SMActiveSession(@NotNull String sessionId, @NotNull Timestamp expiredDate) { +} diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java index 067cc23114..c1242704a1 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java @@ -74,7 +74,7 @@ public class CBDatabase { public static final String SCHEMA_UPDATE_SQL_PATH = "db/cb_schema_update_"; private static final int LEGACY_SCHEMA_VERSION = 1; - private static final int CURRENT_SCHEMA_VERSION = 18; + private static final int CURRENT_SCHEMA_VERSION = 19; private static final String DEFAULT_DB_USER_NAME = "cb-data"; private static final String DEFAULT_DB_PWD_FILE = ".database-credentials.dat"; diff --git a/webapp/packages/core-authentication/src/UserInfoResource.ts b/webapp/packages/core-authentication/src/UserInfoResource.ts index 472cbe2300..fb8b6e6729 100644 --- a/webapp/packages/core-authentication/src/UserInfoResource.ts +++ b/webapp/packages/core-authentication/src/UserInfoResource.ts @@ -26,6 +26,7 @@ export interface ILoginOptions { credentials?: IAuthCredentials; configurationId?: string; linkUser?: boolean; + forceSessionsLogout?: boolean; } export const ANONYMOUS_USER_ID = 'anonymous'; @@ -86,7 +87,7 @@ export class UserInfoResource extends CachedDataResource token.authProvider === providerId); } - async login(provider: string, { credentials, configurationId, linkUser }: ILoginOptions): Promise { + async login(provider: string, { credentials, configurationId, linkUser, forceSessionsLogout }: ILoginOptions): Promise { let processedCredentials: Record | undefined; if (credentials) { @@ -100,6 +101,7 @@ export class UserInfoResource extends CachedDataResource + body { + margin: 0; + font-family: Roboto, sans-serif; + } + + .wrapper { + display: flex; + height: 100vh; + flex-direction: column; + } + + header { + flex-grow: 0; + flex-shrink: 0; + height: 48px; + padding: 0 16px; + background-color: #2a7cb4; + display: flex; + align-items: center; + } + + .logo { + height: 34px; + width: 154px; + } + + .content { + display: flex; + flex-grow: 1; + flex-shrink: 1; + align-items: center; + justify-content: space-around; + } + + .message { + text-align: center; + } + + +
+
+ +
+ +
+
+

Authorization was failed.

+

Now you can close this page.

+
+
+
+ + diff --git a/webapp/packages/core-cli/configs/webpack.product.config.js b/webapp/packages/core-cli/configs/webpack.product.config.js index 2a9c1f7814..07de40317d 100644 --- a/webapp/packages/core-cli/configs/webpack.product.config.js +++ b/webapp/packages/core-cli/configs/webpack.product.config.js @@ -1,3 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ const { resolve } = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin'); @@ -13,6 +20,7 @@ const HtmlInjectWebpackPlugin = require('../utils/HtmlInjectWebpackPlugin.js'); const main = resolve('dist/index.js'); const sso = require.resolve('@cloudbeaver/plugin-sso/dist/index.js'); const ssoHtmlTemplate = require.resolve('@cloudbeaver/plugin-sso/src/index.html.ejs'); +const ssoErrorHtmlTemplate = require.resolve('@cloudbeaver/plugin-sso/src/ssoError.html.ejs'); const outputDir = resolve('lib'); const package = require(resolve('package.json')); const { getServiceWorkerSource } = require('./webpack.product.utils.js'); @@ -98,6 +106,14 @@ module.exports = (env, argv) => { version: timestampVersion, title: package.product?.name, }), + new HtmlWebpackPlugin({ + filename: 'ssoError.html', + template: ssoErrorHtmlTemplate, + inject: 'body', + chunks: ['sso'], + version: timestampVersion, + title: package.product?.name, + }), new HtmlInjectWebpackPlugin({ body: [{ attributes: { hidden: true }, tagName: 'object', innerHTML: '{STATIC_CONTENT}', voidTag: false }], }), diff --git a/webapp/packages/core-cli/configs/webpack.product.dev.config.js b/webapp/packages/core-cli/configs/webpack.product.dev.config.js index bed686351e..e50348ffc5 100644 --- a/webapp/packages/core-cli/configs/webpack.product.dev.config.js +++ b/webapp/packages/core-cli/configs/webpack.product.dev.config.js @@ -16,6 +16,7 @@ const commonConfig = require('./webpack.config.js'); const index = resolve('dist/index.js'); const sso = require.resolve('@cloudbeaver/plugin-sso/dist/index.js'); const ssoHtmlTemplate = require.resolve('@cloudbeaver/plugin-sso/src/index.html.ejs'); +const ssoErrorHtmlTemplate = require.resolve('@cloudbeaver/plugin-sso/src/ssoError.html.ejs'); const { getAssets } = require('./webpack.product.utils'); const HtmlWebpackPlugin = require('html-webpack-plugin'); @@ -126,6 +127,14 @@ module.exports = (env, argv) => { version: package.version, title: package.product?.name, }), + new HtmlWebpackPlugin({ + filename: 'ssoError.html', + template: ssoErrorHtmlTemplate, + inject: 'body', + chunks: ['sso'], + version: package.version, + title: package.product?.name, + }), new HtmlInjectWebpackPlugin({ body: [{ attributes: { hidden: true }, tagName: 'object', innerHTML: '{STATIC_CONTENT}', voidTag: false }], }), diff --git a/webapp/packages/core-sdk/src/EServerErrorCode.ts b/webapp/packages/core-sdk/src/EServerErrorCode.ts index 9727b1a1a9..df0895b90d 100644 --- a/webapp/packages/core-sdk/src/EServerErrorCode.ts +++ b/webapp/packages/core-sdk/src/EServerErrorCode.ts @@ -9,4 +9,5 @@ export enum EServerErrorCode { 'sessionExpired' = 'sessionExpired', 'licenseRequired' = 'licenseRequired', + 'tooManySessions' = 'tooManySessions', } diff --git a/webapp/packages/core-sdk/src/queries/authentication/authLogin.gql b/webapp/packages/core-sdk/src/queries/authentication/authLogin.gql index 7b650bc460..cbb0ea8b7f 100644 --- a/webapp/packages/core-sdk/src/queries/authentication/authLogin.gql +++ b/webapp/packages/core-sdk/src/queries/authentication/authLogin.gql @@ -4,12 +4,14 @@ query authLogin( $credentials: Object $linkUser: Boolean $customIncludeOriginDetails: Boolean! + $forceSessionsLogout: Boolean ) { authInfo: authLogin( - provider: $provider, - configuration: $configuration, - credentials: $credentials, + provider: $provider + configuration: $configuration + credentials: $credentials linkUser: $linkUser + forceSessionsLogout: $forceSessionsLogout ) { redirectLink authId @@ -17,5 +19,5 @@ query authLogin( userTokens { ...AuthToken } - } -} \ No newline at end of file + } +} diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.m.css b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.m.css index a3db0276b3..41269fc0ad 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.m.css +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.m.css @@ -1,3 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ .wrapper { min-height: 520px !important; max-height: max(var(--app-height) - 48px, 520px) !important; @@ -38,3 +45,6 @@ composes: theme-background-secondary theme-text-on-secondary from global; flex: 1; } +.tooManySessionsCheckbox { + margin-bottom: 4px; +} diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx index 6c5c23d9aa..4178054cee 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx @@ -9,10 +9,12 @@ import { observer } from 'mobx-react-lite'; import { AuthProvider, AuthProviderConfiguration, UserInfoResource } from '@cloudbeaver/core-authentication'; import { + Checkbox, CommonDialogBody, CommonDialogFooter, CommonDialogHeader, CommonDialogWrapper, + Container, ErrorMessage, Form, getComputed, @@ -87,6 +89,7 @@ export const AuthDialog: DialogComponent = observer(function async function login(linkUser: boolean, provider?: AuthProvider, configuration?: AuthProviderConfiguration) { try { await dialogData.login(linkUser, provider, configuration); + rejectDialog(); } catch {} } @@ -143,7 +146,7 @@ export const AuthDialog: DialogComponent = observer(function { - state.setTabId(tabData.tabId); + state.switchAuthMode(tabData.tabId); }} > @@ -204,7 +207,7 @@ export const AuthDialog: DialogComponent = observer(function disabled={dialogData.authenticating} onClick={() => { state.setActiveProvider(null, null); - state.setTabId(FEDERATED_AUTH); + state.switchAuthMode(FEDERATED_AUTH); }} > {translate('authentication_auth_federated')} @@ -229,9 +232,25 @@ export const AuthDialog: DialogComponent = observer(function )} - {!federate && ( - - login(linkUser)}> + + + {state.isTooManySessions && ( + { + state.forceSessionsLogout = e.currentTarget.checked; + }} + /> + )} + login(linkUser)} + > {errorDetails.name && ( = observer(function /> )} - - )} + + ); diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogFooter.tsx b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogFooter.tsx index 5aa69b11bf..62c27c4e67 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogFooter.tsx +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogFooter.tsx @@ -24,16 +24,11 @@ export const AuthDialogFooter = observer(function AuthDialogFooter({ auth return (
{children} - + {authAvailable && ( + + )}
); }); diff --git a/webapp/packages/plugin-authentication/src/Dialog/useAuthDialogState.ts b/webapp/packages/plugin-authentication/src/Dialog/useAuthDialogState.ts index 1dee129dbe..17f7bae593 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/useAuthDialogState.ts +++ b/webapp/packages/plugin-authentication/src/Dialog/useAuthDialogState.ts @@ -10,13 +10,14 @@ import { useEffect } from 'react'; import { AdministrationScreenService } from '@cloudbeaver/core-administration'; import { AuthInfoService, AuthProvider, AuthProviderConfiguration, AuthProvidersResource, IAuthCredentials } from '@cloudbeaver/core-authentication'; -import { useObservableRef, useResource } from '@cloudbeaver/core-blocks'; +import { ConfirmationDialog, useObservableRef, useResource } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { NotificationService, UIError } from '@cloudbeaver/core-events'; import type { ITask } from '@cloudbeaver/core-executor'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import type { UserInfo } from '@cloudbeaver/core-sdk'; -import { isArraysEqual } from '@cloudbeaver/core-utils'; +import { EServerErrorCode, GQLError, type UserInfo } from '@cloudbeaver/core-sdk'; +import { errorOf, isArraysEqual } from '@cloudbeaver/core-utils'; import { FEDERATED_AUTH } from './FEDERATED_AUTH'; @@ -42,9 +43,11 @@ interface IState { activeConfiguration: AuthProviderConfiguration | null; credentials: IAuthCredentials; tabIds: string[]; - - setTabId: (tabId: string | null) => void; + isTooManySessions: boolean; + forceSessionsLogout: boolean; + switchAuthMode: (tabId: string | null, resetError?: boolean) => void; setActiveProvider: (provider: AuthProvider | null, configuration: AuthProviderConfiguration | null) => void; + resetErrorState: VoidFunction; } export function useAuthDialogState(accessRequest: boolean, providerId: string | null, configurationId?: string): IData { @@ -52,6 +55,7 @@ export function useAuthDialogState(accessRequest: boolean, providerId: string | const administrationScreenService = useService(AdministrationScreenService); const authInfoService = useService(AuthInfoService); const notificationService = useService(NotificationService); + const commonDialogService = useService(CommonDialogService); const adminPageActive = administrationScreenService.isAdministrationPageActive; const providers = authProvidersResource.data.filter(notEmptyProvider).sort(compareProviders); @@ -110,12 +114,27 @@ export function useAuthDialogState(accessRequest: boolean, providerId: string | profile: '0', credentials: {}, }, - setTabId(tabId: string | null): void { + isTooManySessions: false, + forceSessionsLogout: false, + switchAuthMode(tabId: string | null, resetError = true): void { + if (tabId === this.tabId) { + return; + } + if (tabIds.includes(tabId as any)) { this.tabId = tabId; } else { this.tabId = tabIds[0] ?? null; } + + if (resetError) { + this.resetErrorState(); + } + }, + resetErrorState(): void { + this.isTooManySessions = false; + this.forceSessionsLogout = false; + data.exception = null; }, setActiveProvider(provider: AuthProvider | null, configuration: AuthProviderConfiguration | null): void { const providerChanged = this.activeProvider?.id !== provider?.id; @@ -131,12 +150,12 @@ export function useAuthDialogState(accessRequest: boolean, providerId: string | if (provider) { if (provider.federated) { - this.setTabId(FEDERATED_AUTH); + this.switchAuthMode(FEDERATED_AUTH); } else { - this.setTabId(getAuthProviderTabId(provider, configuration)); + this.switchAuthMode(getAuthProviderTabId(provider, configuration)); } } else { - this.setTabId(null); + this.switchAuthMode(null, false); } }, }), @@ -145,7 +164,11 @@ export function useAuthDialogState(accessRequest: boolean, providerId: string | activeProvider: observable.ref, activeConfiguration: observable.ref, credentials: observable, - setActiveProvider: action, + isTooManySessions: observable.ref, + forceSessionsLogout: observable.ref, + switchAuthMode: action.bound, + setActiveProvider: action.bound, + resetErrorState: action.bound, }, false, ); @@ -177,7 +200,20 @@ export function useAuthDialogState(accessRequest: boolean, providerId: string | return; } + if (state.isTooManySessions && state.forceSessionsLogout) { + const result = await commonDialogService.open(ConfirmationDialog, { + title: 'authentication_auth_force_session_logout_popup_title', + message: 'authentication_auth_force_session_logout_popup_message', + }); + + if (result === DialogueStateResult.Rejected) { + throw new UIError('Force session logout confirmation dialog rejected'); + } + } + this.authenticating = true; + state.isTooManySessions = false; + try { this.state.setActiveProvider(provider, configuration ?? null); @@ -191,17 +227,25 @@ export function useAuthDialogState(accessRequest: boolean, providerId: string | password: state.credentials.credentials.password?.trim(), }, }, + forceSessionsLogout: state.forceSessionsLogout, linkUser, }); this.authTask = loginTask; await loginTask; } catch (exception: any) { + const gqlError = errorOf(exception, GQLError); + + if (gqlError?.errorCode === EServerErrorCode.tooManySessions) { + state.isTooManySessions = true; + } + if (this.destroyed) { notificationService.logException(exception, 'Login failed'); } else { this.exception = exception; } + throw exception; } finally { this.authTask = null; @@ -209,9 +253,11 @@ export function useAuthDialogState(accessRequest: boolean, providerId: string | if (provider.federated) { this.state.setActiveProvider(null, null); - this.state.setTabId(FEDERATED_AUTH); + this.state.switchAuthMode(FEDERATED_AUTH, false); } } + + return; }, }), { diff --git a/webapp/packages/plugin-authentication/src/locales/en.ts b/webapp/packages/plugin-authentication/src/locales/en.ts index a1c94e382f..7c0a60f27c 100644 --- a/webapp/packages/plugin-authentication/src/locales/en.ts +++ b/webapp/packages/plugin-authentication/src/locales/en.ts @@ -1,3 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ export default [ ['authentication_login_dialog_title', 'Authentication'], ['authentication_login', 'Login'], @@ -6,6 +13,10 @@ export default [ ['authentication_authenticate', 'Authenticate'], ['authentication_authorizing', 'Authorizing...'], ['authentication_auth_federated', 'Federated'], + ['authentication_auth_force_session_logout', 'Force all other sessions logout'], + ['authentication_auth_force_session_logout_popup_title', 'Logout other sessions'], + ['authentication_auth_force_session_logout_popup_message', 'All users will be logged out from all of the devices. Do you want to continue?'], + ['authentication_auth_force_session_logout_checkbox_tooltip', 'To apply this option you need to login again'], ['authentication_auth_additional', 'Additional'], ['authentication_select_provider', 'Select available provider'], ['authentication_configure', 'Please configure authentication methods'], diff --git a/webapp/packages/plugin-authentication/src/locales/it.ts b/webapp/packages/plugin-authentication/src/locales/it.ts index d3be9e9de5..e67b11ebfb 100644 --- a/webapp/packages/plugin-authentication/src/locales/it.ts +++ b/webapp/packages/plugin-authentication/src/locales/it.ts @@ -1,3 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ export default [ ['authentication_login_dialog_title', 'Autenticazione'], ['authentication_login', 'Login'], @@ -6,6 +13,10 @@ export default [ ['authentication_authenticate', 'Autentica'], ['authentication_authorizing', 'Authorizing...'], ['authentication_auth_federated', 'Federated'], + ['authentication_auth_force_session_logout', 'Force all other sessions logout'], + ['authentication_auth_force_session_logout_popup_title', 'Logout other sessions'], + ['authentication_auth_force_session_logout_popup_message', 'All users will be logged out from all of the devices. Do you want to continue?'], + ['authentication_auth_force_session_logout_checkbox_tooltip', 'To apply this option you need to login again'], ['authentication_auth_additional', 'Additional'], ['authentication_request_token', 'Autenticazione addizionale richiesta'], ['authentication_user_name', 'Nome utente'], diff --git a/webapp/packages/plugin-authentication/src/locales/ru.ts b/webapp/packages/plugin-authentication/src/locales/ru.ts index 576bb05cd1..9cf2179481 100644 --- a/webapp/packages/plugin-authentication/src/locales/ru.ts +++ b/webapp/packages/plugin-authentication/src/locales/ru.ts @@ -1,3 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ export default [ ['authentication_login_dialog_title', 'Аутентификация'], ['authentication_login', 'Войти'], @@ -6,6 +13,10 @@ export default [ ['authentication_authenticate', 'Аутентифицироваться'], ['authentication_authorizing', 'Авторизация...'], ['authentication_auth_federated', 'Федеративная'], + ['authentication_auth_force_session_logout', 'Принудительный выход из всех сессий'], + ['authentication_auth_force_session_logout_popup_message', 'Текущие сессии для всех пользователей будут прерваны на всех устройствах. Продолжить?'], + ['authentication_auth_force_session_logout_popup_title', 'Выход из всех сессий'], + ['authentication_auth_force_session_logout_checkbox_tooltip', 'Для применения этой опции вам нужно войти снова'], ['authentication_auth_additional', 'Дополнительная'], ['authentication_select_provider', 'Выберите способ аутентификации'], ['authentication_configure', 'Пожалуйста настройте способы аутентификации'], diff --git a/webapp/packages/plugin-authentication/src/locales/zh.ts b/webapp/packages/plugin-authentication/src/locales/zh.ts index 4af3f890b7..a10b1ecd43 100644 --- a/webapp/packages/plugin-authentication/src/locales/zh.ts +++ b/webapp/packages/plugin-authentication/src/locales/zh.ts @@ -1,3 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ export default [ ['authentication_login_dialog_title', '认证'], ['authentication_login', '登录'], @@ -6,6 +13,10 @@ export default [ ['authentication_authenticate', '认证'], ['authentication_authorizing', 'Authorizing...'], ['authentication_auth_federated', '联合认证'], + ['authentication_auth_force_session_logout', 'Force all other sessions logout'], + ['authentication_auth_force_session_logout_popup_title', 'Logout other sessions'], + ['authentication_auth_force_session_logout_popup_message', 'All users will be logged out from all of the devices. Do you want to continue?'], + ['authentication_auth_force_session_logout_checkbox_tooltip', 'To apply this option you need to login again'], ['authentication_auth_additional', 'Additional'], ['authentication_select_provider', '选择可用提供者'], ['authentication_configure', '请配置认证方法'], diff --git a/webapp/packages/plugin-data-import/package.json b/webapp/packages/plugin-data-import/package.json index 80869e3205..c77ac5047a 100644 --- a/webapp/packages/plugin-data-import/package.json +++ b/webapp/packages/plugin-data-import/package.json @@ -19,23 +19,17 @@ }, "dependencies": { "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-connections": "~0.1.0", "@cloudbeaver/core-di": "~0.1.0", "@cloudbeaver/core-dialogs": "~0.1.0", "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-navigation-tree": "~0.1.0", "@cloudbeaver/core-resource": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", "@cloudbeaver/core-settings": "~0.1.0", - "@cloudbeaver/core-ui": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "@cloudbeaver/core-view": "~0.1.0", "@cloudbeaver/plugin-data-viewer": "~0.1.0", - "@cloudbeaver/plugin-datasource-context-switch": "~0.1.0", - "@cloudbeaver/plugin-sql-editor": "~0.1.0", - "@cloudbeaver/plugin-top-app-bar": "~0.1.0", "mobx": "^6.12.0", "mobx-react-lite": "^4.0.5", "react": "^18.2.0" diff --git a/webapp/packages/plugin-data-import/tsconfig.json b/webapp/packages/plugin-data-import/tsconfig.json index 7e5478bb22..3d99affcb0 100644 --- a/webapp/packages/plugin-data-import/tsconfig.json +++ b/webapp/packages/plugin-data-import/tsconfig.json @@ -9,9 +9,6 @@ { "path": "../core-blocks/tsconfig.json" }, - { - "path": "../core-connections/tsconfig.json" - }, { "path": "../core-di/tsconfig.json" }, @@ -24,9 +21,6 @@ { "path": "../core-localization/tsconfig.json" }, - { - "path": "../core-navigation-tree/tsconfig.json" - }, { "path": "../core-resource/tsconfig.json" }, @@ -39,9 +33,6 @@ { "path": "../core-settings/tsconfig.json" }, - { - "path": "../core-ui/tsconfig.json" - }, { "path": "../core-utils/tsconfig.json" }, @@ -50,15 +41,6 @@ }, { "path": "../plugin-data-viewer/tsconfig.json" - }, - { - "path": "../plugin-datasource-context-switch/tsconfig.json" - }, - { - "path": "../plugin-sql-editor/tsconfig.json" - }, - { - "path": "../plugin-top-app-bar/tsconfig.json" } ], "include": [ diff --git a/webapp/packages/plugin-sso/src/ssoError.html.ejs b/webapp/packages/plugin-sso/src/ssoError.html.ejs new file mode 100644 index 0000000000..3be064177a --- /dev/null +++ b/webapp/packages/plugin-sso/src/ssoError.html.ejs @@ -0,0 +1,11 @@ + + + + <%= require('html-loader?{"sources":false}!@cloudbeaver/core-blocks/src/HTMLTemplates/head.html.ejs').default %> + CloudBeaver + + + + <%= require('html-loader?{"sources":false}!@cloudbeaver/core-blocks/src/HTMLTemplates/ssoError.html.ejs').default %> + + \ No newline at end of file diff --git a/webapp/packages/plugin-user-profile-settings/package.json b/webapp/packages/plugin-user-profile-settings/package.json index d76d491567..75cfa569e2 100644 --- a/webapp/packages/plugin-user-profile-settings/package.json +++ b/webapp/packages/plugin-user-profile-settings/package.json @@ -28,7 +28,6 @@ "@cloudbeaver/plugin-settings-menu": "~0.1.0", "@cloudbeaver/plugin-settings-panel": "~0.1.0", "@cloudbeaver/plugin-user-profile": "~0.1.0", - "mobx": "^6.12.0", "mobx-react-lite": "^4.0.5", "react": "^18.2.0" },