From 3bd22885641007ffc5fdff2981070d4eced26b4b Mon Sep 17 00:00:00 2001 From: Alexander Skoblikov Date: Wed, 19 Apr 2023 13:15:27 +0200 Subject: [PATCH] CB-2773-TE-event-pulling-send-session-status-as-an-event (#1596) * CB-3235-Send-event-about-session-expire (#1576) * CB-3235 Send event about session expire * CB-3235 Send event about session expire Fixes WSSessionExpiredEvent description in comment * CB-3235 Send event about session expire Moves WSSocketConnectedEvent to session package * CB-3235 Send event about session expire Implements BaseWebSession#sendSystemEvent * CB-3235 Send event about session expire Implements a WSSessionStateEvent and a job to send it periodically * CB-3235 Send event about session expire Fixes code style issues * CB-2997 jetty server bundle (#1587) * CB-2997 jetty server bundle * CB-2997 remove jetty dependencies * CB-3235 review fixes --------- Co-authored-by: Ainur <59531286+yagudin10@users.noreply.github.com> Co-authored-by: Aleksandr Skoblikov * CB-2773 handle session expire and session state events * CB-2773 fix * CB-2773 subscribe to topic, handle session state event --------- Co-authored-by: Yaroslav Emelyanov Co-authored-by: Ainur <59531286+yagudin10@users.noreply.github.com> Co-authored-by: Tatiana Fokina Co-authored-by: dariamarutkina <125263541+dariamarutkina@users.noreply.github.com> --- .../io/cloudbeaver/auth/CBAuthConstants.java | 3 + .../model/app/WebAuthApplication.java | 6 ++ .../model/session/BaseWebSession.java | 22 +++++- .../model/session/WebHeadlessSession.java | 4 +- .../cloudbeaver/model/session/WebSession.java | 34 ++-------- .../schema/service.events.graphqls | 16 +++++ .../io/cloudbeaver/server/CBApplication.java | 11 ++- .../io/cloudbeaver/server/CBConstants.java | 3 - .../src/io/cloudbeaver/server/CBPlatform.java | 8 ++- .../server/jetty/CBJettyServer.java | 2 +- .../PeriodicSystemJob.java} | 43 ++++++------ .../server/jobs/SessionStateJob.java | 40 +++++++++++ .../server/jobs/WebSessionMonitorJob.java | 43 ++++++++++++ .../server/websockets/CBEventsWebSocket.java | 2 +- .../service/core/impl/WebServiceCore.java | 11 ++- .../service/session/WebSessionManager.java | 35 +++++----- .../src/SessionExpireEventService.ts | 30 +++++++++ .../core-root/src/SessionInfoEventHandler.ts | 29 ++++++++ .../packages/core-root/src/SessionResource.ts | 15 +++++ webapp/packages/core-root/src/index.ts | 1 + webapp/packages/core-root/src/manifest.ts | 4 ++ webapp/packages/core-sdk/src/sdk.ts | 14 ++++ .../SessionExpireWarningDialogService.ts | 67 ++++++++----------- 23 files changed, 323 insertions(+), 120 deletions(-) rename server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/{WebSessionMonitorJob.java => jobs/PeriodicSystemJob.java} (61%) create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebSessionMonitorJob.java create mode 100644 webapp/packages/core-root/src/SessionExpireEventService.ts create mode 100644 webapp/packages/core-root/src/SessionInfoEventHandler.ts 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 e5894bac51..2bccdc5f71 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,4 +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"; + + // 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/app/WebAuthApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAuthApplication.java index 27291ebceb..8ac69c9f3f 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAuthApplication.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAuthApplication.java @@ -17,8 +17,14 @@ package io.cloudbeaver.model.app; +import io.cloudbeaver.auth.CBAuthConstants; + public interface WebAuthApplication extends WebApplication { WebAuthConfiguration getAuthConfiguration(); String getAuthServiceURL(); + + default long getMaxSessionIdleTime() { + return CBAuthConstants.MAX_SESSION_IDLE_TIME; + } } 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 94549fea1d..64b96c9479 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 @@ -17,6 +17,7 @@ package io.cloudbeaver.model.session; import io.cloudbeaver.model.app.WebApplication; +import io.cloudbeaver.model.app.WebAuthApplication; import io.cloudbeaver.websocket.CBWebSessionEventHandler; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; @@ -28,6 +29,7 @@ import org.jkiss.dbeaver.model.auth.SMSessionContext; import org.jkiss.dbeaver.model.auth.impl.AbstractSessionPersistent; import org.jkiss.dbeaver.model.meta.Property; import org.jkiss.dbeaver.model.websocket.event.WSEvent; +import org.jkiss.dbeaver.model.websocket.event.session.WSSessionExpiredEvent; import java.time.Instant; import java.time.LocalDateTime; @@ -47,14 +49,14 @@ public abstract class BaseWebSession extends AbstractSessionPersistent { @NotNull protected final WebUserContext userContext; @NotNull - protected final WebApplication application; + protected final WebAuthApplication application; protected volatile long lastAccessTime; private final List sessionEventHandlers = new CopyOnWriteArrayList<>(); private WebSessionEventsFilter eventsFilter = new WebSessionEventsFilter(); private final WebSessionWorkspace workspace; - public BaseWebSession(@NotNull String id, @NotNull WebApplication application) throws DBException { + public BaseWebSession(@NotNull String id, @NotNull WebAuthApplication application) throws DBException { this.id = id; this.application = application; this.createTime = System.currentTimeMillis(); @@ -152,8 +154,14 @@ public abstract class BaseWebSession extends AbstractSessionPersistent { @Override public void close() { super.close(); + var sessionExpiredEvent = new WSSessionExpiredEvent(); synchronized (sessionEventHandlers) { for (CBWebSessionEventHandler sessionEventHandler : sessionEventHandlers) { + try { + sessionEventHandler.handeWebSessionEvent(sessionExpiredEvent); + } catch (DBException e) { + log.warn("Failed to send session expiration event", e); + } sessionEventHandler.close(); } sessionEventHandlers.clear(); @@ -182,4 +190,14 @@ public abstract class BaseWebSession extends AbstractSessionPersistent { public void removeSessionProject(@Nullable String projectId) throws DBException { userContext.getAccessibleProjectIds().remove(projectId); } + + @Property + public boolean isValid() { + return getRemainingTime() > 0; + } + + @Property + public long getRemainingTime() { + return application.getMaxSessionIdleTime() + lastAccessTime - System.currentTimeMillis(); + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHeadlessSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHeadlessSession.java index fffc260a8b..9f8856507b 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHeadlessSession.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHeadlessSession.java @@ -16,7 +16,7 @@ */ package io.cloudbeaver.model.session; -import io.cloudbeaver.model.app.WebApplication; +import io.cloudbeaver.model.app.WebAuthApplication; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.auth.SMSessionPrincipal; @@ -27,7 +27,7 @@ import org.jkiss.dbeaver.model.auth.SMSessionPrincipal; public class WebHeadlessSession extends BaseWebSession { public WebHeadlessSession( @NotNull String id, - @NotNull WebApplication application + @NotNull WebAuthApplication application ) throws DBException { super(id, application); } 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 f401609dad..f45dadfc3a 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 @@ -26,7 +26,7 @@ import io.cloudbeaver.WebProjectImpl; import io.cloudbeaver.model.WebAsyncTaskInfo; import io.cloudbeaver.model.WebConnectionInfo; import io.cloudbeaver.model.WebServerMessage; -import io.cloudbeaver.model.app.WebApplication; +import io.cloudbeaver.model.app.WebAuthApplication; import io.cloudbeaver.model.rm.RMUtils; import io.cloudbeaver.model.user.WebUser; import io.cloudbeaver.service.DBWSessionHandler; @@ -71,14 +71,14 @@ import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.dbeaver.runtime.jobs.DisconnectJob; import org.jkiss.utils.CommonUtils; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; import java.lang.reflect.InvocationTargetException; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.stream.Collectors; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; /** * Web session. @@ -97,7 +97,6 @@ public class WebSession extends BaseWebSession private final AtomicInteger taskCount = new AtomicInteger(); - private long maxSessionIdleTime; private String lastRemoteAddr; private String lastRemoteUserAgent; @@ -121,15 +120,13 @@ public class WebSession extends BaseWebSession public WebSession( @NotNull HttpSession httpSession, - @NotNull WebApplication application, - @NotNull Map sessionHandlers, - long maxSessionIdleTime + @NotNull WebAuthApplication application, + @NotNull Map sessionHandlers ) throws DBException { super(httpSession.getId(), application); this.lastAccessTime = this.createTime; setLocale(CommonUtils.toString(httpSession.getAttribute(ATTR_LOCALE), this.locale)); this.sessionHandlers = sessionHandlers; - this.maxSessionIdleTime = maxSessionIdleTime; } @Override @@ -483,14 +480,12 @@ public class WebSession extends BaseWebSession public synchronized void updateInfo( HttpServletRequest request, - HttpServletResponse response, - long maxSessionIdleTime + HttpServletResponse response ) throws DBWebException { touchSession(); HttpSession httpSession = request.getSession(); this.lastRemoteAddr = request.getRemoteAddr(); this.lastRemoteUserAgent = request.getHeader("User-Agent"); - this.maxSessionIdleTime = maxSessionIdleTime; this.cacheExpired = false; if (!httpSession.isNew()) { try { @@ -996,21 +991,6 @@ public class WebSession extends BaseWebSession } } - @Property - public boolean isValid() { - return getSessionActiveTimeLeft() > 0; - } - - @Property - public long getRemainingTime() { - return getSessionActiveTimeLeft(); - } - - private long getSessionActiveTimeLeft() { - return maxSessionIdleTime + lastAccessTime - System.currentTimeMillis(); - } - - private class SessionProgressMonitor extends BaseProgressMonitor { @Override public void beginTask(String name, int totalWork) { diff --git a/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls index 59168b6437..4c372ea66d 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls @@ -9,6 +9,8 @@ enum CBServerEventId { cb_session_log_updated, cb_session_websocket_connected, + cb_session_state, + cb_session_expired, cb_datasource_created, cb_datasource_updated, @@ -99,6 +101,20 @@ type WSSocketConnectedEvent implements CBServerEvent { applicationRunId: String! } +# Session state info event +type WSSessionStateEvent implements CBServerEvent { + id: CBServerEventId! + topicId: CBEventTopic + remainingTime: Int! + isValid: Boolean +} + +# Session expired event +type WSSessionExpiredEvent implements CBServerEvent { + id: CBServerEventId! + topicId: CBEventTopic +} + # RM project update event type CBProjectUpdateEvent implements CBServerEvent { id: CBServerEventId! diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java index 762de71ecd..ca2f602a57 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java @@ -20,6 +20,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.InstanceCreator; import io.cloudbeaver.WebServiceUtils; +import io.cloudbeaver.auth.CBAuthConstants; import io.cloudbeaver.model.app.BaseWebApplication; import io.cloudbeaver.model.app.WebAuthApplication; import io.cloudbeaver.model.app.WebAuthConfiguration; @@ -85,6 +86,11 @@ public abstract class CBApplication extends BaseWebApplication implements WebAut private static final Log log = Log.getLog(CBApplication.class); private static final boolean RECONFIGURATION_ALLOWED = true; + /** + * In configuration mode sessions expire after a week + */ + private static final long CONFIGURATION_MODE_SESSION_IDLE_TIME = 60 * 60 * 1000 * 24 * 7; + static { Log.setDefaultDebugStream(System.out); @@ -119,7 +125,7 @@ public abstract class CBApplication extends BaseWebApplication implements WebAut // Persistence protected SMAdminController securityController; - private long maxSessionIdleTime = CBConstants.MAX_SESSION_IDLE_TIME; + private long maxSessionIdleTime = CBAuthConstants.MAX_SESSION_IDLE_TIME; private boolean develMode = false; private boolean configurationMode = false; @@ -179,6 +185,9 @@ public abstract class CBApplication extends BaseWebApplication implements WebAut } public long getMaxSessionIdleTime() { + if (isConfigurationMode()) { + return CONFIGURATION_MODE_SESSION_IDLE_TIME; + } return maxSessionIdleTime; } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java index ff6dec4d45..8ecbcfacdf 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java @@ -63,9 +63,6 @@ public class CBConstants { public static final String DEFAULT_ADMIN_NAME = "cbadmin"; public static final String DEFAULT_ADMIN_TEAM = "admin"; - // Default max idle time (10 minutes) - public static final long MAX_SESSION_IDLE_TIME = 10 * 60 * 1000; - public static final String ENV_CB_HOME = "CLOUDBEAVER_HOME"; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java index dd73151179..5797a7e871 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java @@ -17,6 +17,8 @@ package io.cloudbeaver.server; +import io.cloudbeaver.server.jobs.SessionStateJob; +import io.cloudbeaver.server.jobs.WebSessionMonitorJob; import io.cloudbeaver.service.session.WebSessionManager; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.Platform; @@ -148,7 +150,11 @@ public class CBPlatform extends BasePlatformImpl { refreshApplicableDrivers(); - new WebSessionMonitorJob(this).scheduleMonitor(); + new WebSessionMonitorJob(this) + .scheduleMonitor(); + + new SessionStateJob(this) + .scheduleMonitor(); log.info("Web platform initialized (" + (System.currentTimeMillis() - startTime) + "ms)"); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java index 37cca77b4e..c3c7f1a257 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java @@ -186,7 +186,7 @@ public class CBJettyServer { return 1; } }*/; - var maxIdleSeconds = application.getSessionManager().getMaxSessionIdleTime(); + var maxIdleSeconds = application.getMaxSessionIdleTime(); int intMaxIdleSeconds; if (maxIdleSeconds > Integer.MAX_VALUE) { log.warn("Max session idle time value is greater than Integer.MAX_VALUE. Integer.MAX_VALUE will be used instead"); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebSessionMonitorJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/PeriodicSystemJob.java similarity index 61% rename from server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebSessionMonitorJob.java rename to server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/PeriodicSystemJob.java index 3b56ac8891..64854916aa 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebSessionMonitorJob.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/PeriodicSystemJob.java @@ -14,50 +14,49 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.cloudbeaver.server; +package io.cloudbeaver.server.jobs; +import io.cloudbeaver.server.CBPlatform; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; -import org.jkiss.dbeaver.Log; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.model.runtime.AbstractJob; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; -/** - * WebSessionMonitorJob - */ -class WebSessionMonitorJob extends AbstractJob { - private static final Log log = Log.getLog(WebSessionMonitorJob.class); - private static final int MONITOR_INTERVAL = 10000; // once per 10 seconds +public abstract class PeriodicSystemJob extends AbstractJob { - private final CBPlatform platform; + @NotNull + protected final CBPlatform platform; + private final long periodMs; + + public PeriodicSystemJob(@NotNull String name, @NotNull CBPlatform platform, long periodMs) { + super(name); + this.platform = platform; + this.periodMs = periodMs; - WebSessionMonitorJob(CBPlatform platform) { - super("Web session monitor"); setUser(false); setSystem(true); - this.platform = platform; } @Override - protected IStatus run(DBRProgressMonitor monitor) { + protected IStatus run(@NotNull DBRProgressMonitor monitor) { if (platform.isShuttingDown()) { return Status.OK_STATUS; } - try { - platform.getSessionManager().expireIdleSessions(); - } catch (Exception e) { - log.error("Error on expire idle sessions", e); - } + doJob(monitor); + // If the platform is still running after the job is completed, reschedule the job if (!platform.isShuttingDown()) { scheduleMonitor(); } + return Status.OK_STATUS; } - void scheduleMonitor() { - schedule(MONITOR_INTERVAL); - } + protected abstract void doJob(@NotNull DBRProgressMonitor monitor); -} \ No newline at end of file + public void scheduleMonitor() { + schedule(periodMs); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java new file mode 100644 index 0000000000..15ffe85b79 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java @@ -0,0 +1,40 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2023 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.jobs; + +import io.cloudbeaver.server.CBPlatform; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; + +public class SessionStateJob extends PeriodicSystemJob { + private static final Log log = Log.getLog(SessionStateJob.class); + private static final int PERIOD_MS = 60_000; // once per 60 seconds + + public SessionStateJob(@NotNull CBPlatform platform) { + super("Session state sender", platform, PERIOD_MS); + } + + @Override + protected void doJob(@NotNull DBRProgressMonitor monitor) { + try { + platform.getSessionManager().sendSessionsStates(); + } catch (Exception e) { + log.error("Error sending session state", e); + } + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebSessionMonitorJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebSessionMonitorJob.java new file mode 100644 index 0000000000..6622c303d4 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebSessionMonitorJob.java @@ -0,0 +1,43 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2023 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.jobs; + +import io.cloudbeaver.server.CBPlatform; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; + +/** + * WebSessionMonitorJob + */ +public class WebSessionMonitorJob extends PeriodicSystemJob { + private static final Log log = Log.getLog(WebSessionMonitorJob.class); + private static final int MONITOR_INTERVAL = 10000; // once per 10 seconds + + public WebSessionMonitorJob(@NotNull CBPlatform platform) { + super("Web session monitor", platform, MONITOR_INTERVAL); + } + + @Override + protected void doJob(@NotNull DBRProgressMonitor monitor) { + try { + platform.getSessionManager().expireIdleSessions(); + } catch (Exception e) { + log.error("Error on expire idle sessions", e); + } + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java index 0caee3e81b..a71c589f74 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java @@ -29,8 +29,8 @@ import org.jkiss.dbeaver.model.websocket.WSUtils; import org.jkiss.dbeaver.model.websocket.event.WSClientEvent; import org.jkiss.dbeaver.model.websocket.event.WSClientEventType; import org.jkiss.dbeaver.model.websocket.event.WSEvent; -import org.jkiss.dbeaver.model.websocket.event.WSSocketConnectedEvent; import org.jkiss.dbeaver.model.websocket.event.client.WSUpdateActiveProjectsClientEvent; +import org.jkiss.dbeaver.model.websocket.event.session.WSSocketConnectedEvent; import java.io.IOException; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java index bf79f39107..10a0eacfa8 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java @@ -50,7 +50,6 @@ import org.jkiss.dbeaver.model.net.DBWTunnel; import org.jkiss.dbeaver.model.net.ssh.SSHImplementation; import org.jkiss.dbeaver.model.rm.RMProjectType; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; -import org.jkiss.dbeaver.model.secret.DBSSecretController; import org.jkiss.dbeaver.model.websocket.WSConstants; import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty; import org.jkiss.dbeaver.registry.DataSourceDescriptor; @@ -61,13 +60,13 @@ import org.jkiss.dbeaver.runtime.jobs.ConnectionTestJob; import org.jkiss.dbeaver.utils.RuntimeUtils; import org.jkiss.utils.CommonUtils; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; /** * Web service implementation @@ -235,6 +234,12 @@ public class WebServiceCore implements DBWServiceCore { return webSession; } + /** + * Updates the user's permissions + * + * @deprecated CB-2773. The actual way to get session state is {@code WSSessionStateEvent} which sends periodically via web socket. + */ + @Deprecated @Override public WebSession getSessionState(@NotNull WebSession webSession) throws DBWebException { try { 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 229f00e67c..fc4487f6d9 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 @@ -25,7 +25,6 @@ import io.cloudbeaver.model.session.WebSessionAuthProcessor; import io.cloudbeaver.registry.WebHandlerRegistry; import io.cloudbeaver.registry.WebSessionHandlerDescriptor; import io.cloudbeaver.server.CBApplication; -import io.cloudbeaver.server.CBPlatform; import io.cloudbeaver.service.DBWSessionHandler; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; @@ -34,7 +33,7 @@ import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.auth.SMAuthInfo; import org.jkiss.dbeaver.model.security.user.SMAuthPermissions; import org.jkiss.dbeaver.model.websocket.WSConstants; -import org.jkiss.dbeaver.runtime.DBWorkbench; +import org.jkiss.dbeaver.model.websocket.event.session.WSSessionStateEvent; import org.jkiss.utils.CommonUtils; import java.util.*; @@ -50,11 +49,6 @@ public class WebSessionManager { private static final Log log = Log.getLog(WebSessionManager.class); - /** - * In configuration mode sessions expire after a week - */ - private static final long CONFIGURATION_MODE_SESSION_IDLE_TIME = 60 * 60 * 1000 * 24 * 7; - private final CBApplication application; private final Map sessionMap = new HashMap<>(); @@ -88,8 +82,7 @@ public class WebSessionManager { public boolean touchSession(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response) throws DBWebException { WebSession webSession = getWebSession(request, response, false); - long maxSessionIdleTime = CBApplication.getInstance().getMaxSessionIdleTime(); - webSession.updateInfo(request, response, maxSessionIdleTime); + webSession.updateInfo(request, response); return true; } @@ -149,7 +142,7 @@ public class WebSessionManager { if (updateInfo) { // Update only once per request if (!CommonUtils.toBoolean(request.getAttribute("sessionUpdated"))) { - webSession.updateInfo(request, response, application.getMaxSessionIdleTime()); + webSession.updateInfo(request, response); request.setAttribute("sessionUpdated", true); } } @@ -224,7 +217,7 @@ public class WebSessionManager { @NotNull protected WebSession createWebSessionImpl(@NotNull HttpSession httpSession) throws DBException { - return new WebSession(httpSession, application, getSessionHandlers(), application.getMaxSessionIdleTime()); + return new WebSession(httpSession, application, getSessionHandlers()); } @NotNull @@ -264,14 +257,8 @@ public class WebSessionManager { return null; } - public long getMaxSessionIdleTime() { - return CBApplication.getInstance().isConfigurationMode() - ? CONFIGURATION_MODE_SESSION_IDLE_TIME - : DBWorkbench.getPlatform(CBPlatform.class).getApplication().getMaxSessionIdleTime(); - } - public void expireIdleSessions() { - long maxSessionIdleTime = getMaxSessionIdleTime(); + long maxSessionIdleTime = application.getMaxSessionIdleTime(); List expiredList = new ArrayList<>(); synchronized (sessionMap) { @@ -335,4 +322,16 @@ public class WebSessionManager { return headlessSession; } } + + /** + * Send session state with remaining alive time to all cached session + */ + public void sendSessionsStates() throws DBException { + synchronized (sessionMap) { + for (var session : sessionMap.values()) { + session.getUserContext().refreshPermissions(); + session.addSessionEvent(new WSSessionStateEvent(session.getRemainingTime(), session.isValid())); + } + } + } } diff --git a/webapp/packages/core-root/src/SessionExpireEventService.ts b/webapp/packages/core-root/src/SessionExpireEventService.ts new file mode 100644 index 0000000000..478f34737f --- /dev/null +++ b/webapp/packages/core-root/src/SessionExpireEventService.ts @@ -0,0 +1,30 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { Dependency, injectable } from '@cloudbeaver/core-di'; +import type { WsSessionExpiredEvent } from '@cloudbeaver/core-sdk'; + +import { ServerEventId, SessionEventSource } from './SessionEventSource'; +import { SessionExpireService } from './SessionExpireService'; + +@injectable() +export class SessionExpireEventService extends Dependency { + constructor( + private readonly sessionEventSource: SessionEventSource, + private readonly sessionExpireService: SessionExpireService, + ) { + super(); + this.sessionEventSource.onEvent(ServerEventId.CbSessionExpired, () => { + this.onSessionExpireEvent(); + }); + } + + private onSessionExpireEvent(): void { + this.sessionExpireService.sessionExpired(); + } +} diff --git a/webapp/packages/core-root/src/SessionInfoEventHandler.ts b/webapp/packages/core-root/src/SessionInfoEventHandler.ts new file mode 100644 index 0000000000..1f11c7fb7e --- /dev/null +++ b/webapp/packages/core-root/src/SessionInfoEventHandler.ts @@ -0,0 +1,29 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { injectable } from '@cloudbeaver/core-di'; +import type { WsSessionStateEvent as ISessionStateEvent } from '@cloudbeaver/core-sdk'; + +import { TopicEventHandler } from './ServerEventEmitter/TopicEventHandler'; +import { ISessionEvent, SessionEventId, SessionEventSource, SessionEventTopic } from './SessionEventSource'; + +export { type ISessionStateEvent }; + +@injectable() +export class SessionInfoEventHandler + extends TopicEventHandler { + constructor( + sessionEventSource: SessionEventSource + ) { + super(SessionEventTopic.CbSession, sessionEventSource); + } + + map(event: any): ISessionStateEvent { + return event; + } +} diff --git a/webapp/packages/core-root/src/SessionResource.ts b/webapp/packages/core-root/src/SessionResource.ts index c5c646e6a0..ae79d06541 100644 --- a/webapp/packages/core-root/src/SessionResource.ts +++ b/webapp/packages/core-root/src/SessionResource.ts @@ -7,6 +7,7 @@ */ import { injectable } from '@cloudbeaver/core-di'; +import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; import { GraphQLService, CachedDataResource, @@ -14,6 +15,8 @@ import { } from '@cloudbeaver/core-sdk'; import { ServerConfigResource } from './ServerConfigResource'; +import { ServerEventId } from './SessionEventSource'; +import { SessionInfoEventHandler } from './SessionInfoEventHandler'; export type SessionState = SessionStateFragment; export interface ISessionAction { @@ -21,17 +24,29 @@ export interface ISessionAction { [key: string]: any; } +interface SessionStateData { + isValid?: boolean; + remainingTime: number; +} + @injectable() export class SessionResource extends CachedDataResource { private action: ISessionAction | null; private defaultLocale: string | undefined; + readonly onStatusUpdate: ISyncExecutor; constructor( private readonly graphQLService: GraphQLService, + sessionInfoEventHandler: SessionInfoEventHandler, serverConfigResource: ServerConfigResource ) { super(() => null); + this.onStatusUpdate = new SyncExecutor(); + sessionInfoEventHandler.onEvent(ServerEventId.CbSessionState, event => { + this.onStatusUpdate.execute(event); + }, undefined, this); + this.action = null; this.sync(serverConfigResource, () => {}, () => {}); } diff --git a/webapp/packages/core-root/src/index.ts b/webapp/packages/core-root/src/index.ts index e2a30cd2f6..a9a0270d79 100644 --- a/webapp/packages/core-root/src/index.ts +++ b/webapp/packages/core-root/src/index.ts @@ -21,6 +21,7 @@ export * from './SessionActionService'; export * from './SessionDataResource'; export * from './SessionSettingsService'; export * from './SessionExpireService'; +export * from './SessionExpireEventService'; export * from './ServerNodeService'; export * from './WindowEventsService'; export { manifest as coreRootManifest } from './manifest'; diff --git a/webapp/packages/core-root/src/manifest.ts b/webapp/packages/core-root/src/manifest.ts index 3cbb6d6420..114eda3ebc 100644 --- a/webapp/packages/core-root/src/manifest.ts +++ b/webapp/packages/core-root/src/manifest.ts @@ -21,7 +21,9 @@ import { ServerSettingsService } from './ServerSettingsService'; import { SessionActionService } from './SessionActionService'; import { SessionDataResource } from './SessionDataResource'; import { SessionEventSource } from './SessionEventSource'; +import { SessionExpireEventService } from './SessionExpireEventService'; import { SessionExpireService } from './SessionExpireService'; +import { SessionInfoEventHandler } from './SessionInfoEventHandler'; import { SessionPermissionsResource } from './SessionPermissionsResource'; import { SessionResource } from './SessionResource'; import { SessionService } from './SessionService'; @@ -45,6 +47,7 @@ export const manifest: PluginManifest = { SessionActionService, SessionDataResource, SessionExpireService, + SessionExpireEventService, ServerNodeService, SessionResource, SessionService, @@ -53,6 +56,7 @@ export const manifest: PluginManifest = { QuotasService, ServerConfigEventHandler, SessionEventSource, + SessionInfoEventHandler, DataSynchronizationService, ], }; diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index d392363af5..3bc9c214fb 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -249,7 +249,9 @@ export enum CbServerEventId { CbRmResourceCreated = 'cb_rm_resource_created', CbRmResourceDeleted = 'cb_rm_resource_deleted', CbRmResourceUpdated = 'cb_rm_resource_updated', + CbSessionExpired = 'cb_session_expired', CbSessionLogUpdated = 'cb_session_log_updated', + CbSessionState = 'cb_session_state', CbSessionWebsocketConnected = 'cb_session_websocket_connected' } @@ -1732,6 +1734,18 @@ export interface UserInfo { userId: Scalars['ID']; } +export interface WsSessionExpiredEvent extends CbServerEvent { + id: CbServerEventId; + topicId?: Maybe; +} + +export interface WsSessionStateEvent extends CbServerEvent { + id: CbServerEventId; + isValid?: Maybe; + remainingTime: Scalars['Int']; + topicId?: Maybe; +} + export interface WsSocketConnectedEvent extends CbServerEvent { applicationRunId: Scalars['String']; id: CbServerEventId; diff --git a/webapp/packages/plugin-root/src/SessionExpireWarningDialog/SessionExpireWarningDialogService.ts b/webapp/packages/plugin-root/src/SessionExpireWarningDialog/SessionExpireWarningDialogService.ts index d7d240b587..bfb742779d 100644 --- a/webapp/packages/plugin-root/src/SessionExpireWarningDialog/SessionExpireWarningDialogService.ts +++ b/webapp/packages/plugin-root/src/SessionExpireWarningDialog/SessionExpireWarningDialogService.ts @@ -15,7 +15,6 @@ import { GraphQLService } from '@cloudbeaver/core-sdk'; import { SessionExpireWarningDialog } from './SessionExpireWarningDialog'; const WARN_IN = 5 * 1000 * 60; -const POLL_INTERVAL = 1 * 1000 * 60; @injectable() export class SessionExpireWarningDialogService extends Bootstrap { @@ -27,7 +26,7 @@ export class SessionExpireWarningDialogService extends Bootstrap { private readonly serverConfigResource: ServerConfigResource, private readonly sessionResource: SessionResource, private readonly userInfoResource: UserInfoResource, - private readonly graphQLService: GraphQLService + private readonly graphQLService: GraphQLService, ) { super(); this.dialogInternalPromise = null; @@ -35,50 +34,40 @@ export class SessionExpireWarningDialogService extends Bootstrap { register(): void { this.sessionExpireService.onSessionExpire.addHandler(this.close.bind(this)); + this.sessionResource.onStatusUpdate.addHandler((data, contexts) => { + this.handleStateChange(data.isValid, data.remainingTime); + }); } - load(): void { - this.startSessionPolling(); - } + load(): void { } - private startSessionPolling() { - const checkSessionStatus = async () => { - if ( - !this.serverConfigResource.anonymousAccessEnabled - && !this.userInfoResource.data - && !this.serverConfigResource.configurationMode - ) { - return; - } + private handleStateChange(isValid?: boolean, remainingTime?: number) { + if ( + !this.serverConfigResource.anonymousAccessEnabled + && !this.userInfoResource.data + && !this.serverConfigResource.configurationMode + ) { + return; + } - const { sessionState } = await this.graphQLService.sdk.sessionState(); + if (!isValid) { + this.close(); + this.sessionExpireService.sessionExpired(); + return; + } - if (!sessionState.valid) { - this.close(); - this.sessionExpireService.sessionExpired(); - return; - } + const sessionDuration = this.serverConfigResource.data?.sessionExpireTime; - const sessionDuration = this.serverConfigResource.data?.sessionExpireTime; + if (this.sessionExpireService.expired || !sessionDuration || sessionDuration < WARN_IN) { + this.close(); + return; + } - if (this.sessionExpireService.expired || !sessionDuration || sessionDuration < WARN_IN) { - this.close(); - return; - } - - if (sessionState.remainingTime < WARN_IN) { - this.open(); - } else { - this.close(); - } - }; - - const poll = async () => { - await checkSessionStatus(); - setTimeout(poll, POLL_INTERVAL); - }; - - setTimeout(poll, POLL_INTERVAL); + if (remainingTime !== undefined && remainingTime < WARN_IN) { + this.open(); + } else { + this.close(); + } } private async open(): Promise {