From 837d321b3f9d95f68aff596b738aa0b708b2633f Mon Sep 17 00:00:00 2001 From: Ainur <59531286+yagudin10@users.noreply.github.com> Date: Thu, 23 Mar 2023 14:42:08 +0100 Subject: [PATCH] CB-2888 project sync (#1508) * CB-3115 Add new create/delete project events Implements new events * CB-3115 Add new create/delete project events Renames WSProjectLifeCycleEvent to WSProjectLifeCycleEvent Adds BaseWebSession#addSessionEvent call in WsProjectLifeCycleEventHandler * CB-3127 events for project permissions * CB-2888 TE: Add synchronization for projects Renames WSProjectUpdatedEventHandler to WSAbstractProjectEventHandler and WsProjectLifeCycleEventHandler to WSProjectUpdatedEventHandler * CB-3127 code style fix * CB-3127 rename parameters * CB-3114 support project remove/add event * CB-3127 event filter fix * CB-2888 TE: Add synchronization for projects Moves validateEvent call to WSAbstractProjectEventHandler; Adds the use of an implementation class logger to the WSAbstractProjectEventHandler. * CB-2888 TE: Add synchronization for projects Fixes missed copyright * CB-3127 filter event fix * CB-2888 fix type naming * CB-3127 project update event add remove connections from session * CB-2888 TE: Add synchronization for projects Adds dispatch of project creation/deletion events to WebServiceRM * CB-3127 load connections when creating web project * CB-3114 update active projects on active project remove * CB-3114 update active projects via handler on project delete * CB-2888 mark root tree as outdated on project add event * CB-2888 move tree outdate logic to the useElementsTree hook * CB-2888 fix conflict with devel --------- Co-authored-by: Iaroslav Emelianov Co-authored-by: naumov Co-authored-by: dariamarutkina <125263541+dariamarutkina@users.noreply.github.com> Co-authored-by: Alexander Skoblikov --- .../rm/local/LocalResourceController.java | 2 +- .../model/session/BaseWebSession.java | 9 ++ .../cloudbeaver/model/session/WebSession.java | 67 +++++------ .../model/session/WebSessionEventsFilter.java | 6 +- .../bundles/io.cloudbeaver.server/plugin.xml | 2 + .../schema/service.events.graphqls | 23 +++- .../events/WSAbstractProjectEventHandler.java | 74 ++++++++++++ .../WSDataSourceUpdatedEventHandlerImpl.java | 38 +++--- .../WSFolderUpdatedEventHandlerImpl.java | 35 ++++-- .../WSPermissionUpdatedEventHandler.java | 110 ++++++++++++++++++ .../events/WSProjectUpdatedEventHandler.java | 58 ++++++--- .../WSRmResourceUpdatedEventHandlerImpl.java | 51 ++++---- .../events/WSUserSecretEventHandlerImpl.java | 18 ++- .../service/rm/impl/WebServiceRM.java | 27 ++++- .../src/ProjectInfoEventHandler.ts | 8 +- .../core-projects/src/ProjectsService.ts | 34 +++++- webapp/packages/core-sdk/src/sdk.ts | 14 +++ .../ElementsTree/useElementsTree.ts | 10 +- 18 files changed, 459 insertions(+), 127 deletions(-) create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSAbstractProjectEventHandler.java create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSPermissionUpdatedEventHandler.java diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java index 1fbbd8f06b..80218b7988 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java @@ -190,7 +190,7 @@ public class LocalResourceController implements RMController { .collect(Collectors.toList()); } - private Set getProjectPermissions(@Nullable String projectId, RMProjectType projectType) throws DBException { + private Set getProjectPermissions(@Nullable String projectId, @NotNull RMProjectType projectType) throws DBException { var activeUserCreds = credentialsProvider.getActiveUserCredentials(); switch (projectType) { 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 0920a089ca..8beb469940 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 @@ -19,6 +19,7 @@ package io.cloudbeaver.model.session; import io.cloudbeaver.model.app.WebApplication; import io.cloudbeaver.websocket.CBWebSessionEventHandler; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.auth.SMAuthInfo; @@ -168,4 +169,12 @@ public abstract class BaseWebSession extends AbstractSessionPersistent { public boolean isProjectAccessible(String projectId) { return userContext.getAccessibleProjectIds().contains(projectId); } + + public void addSessionProject(@NotNull String projectId) throws DBException { + userContext.getAccessibleProjectIds().add(projectId); + } + + public void removeSessionProject(@Nullable String projectId) throws DBException { + userContext.getAccessibleProjectIds().remove(projectId); + } } 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 ca63d1c19c..3ef7c19edb 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 @@ -302,6 +302,7 @@ public class WebSession extends BaseWebSession this.navigatorModel.dispose(); this.navigatorModel = null; } + this.connections.clear(); loadProjects(); @@ -310,13 +311,6 @@ public class WebSession extends BaseWebSession this.navigatorModel.initialize(); this.locale = Locale.getDefault().getLanguage(); - - try { - this.refreshConnections(); - } catch (Exception e) { - addSessionError(e); - log.error("Error getting connection list", e); - } } private void loadProjects() { @@ -360,35 +354,17 @@ public class WebSession extends BaseWebSession if (!project.isShared() || application.isConfigurationMode()) { getWorkspace().setActiveProject(sessionProject); } + for (DBPDataSourceContainer ds : dataSourceRegistry.getDataSources()) { + addConnection(new WebConnectionInfo(this, ds)); + } + Throwable lastError = dataSourceRegistry.getLastError(); + if (lastError != null) { + addSessionError(lastError); + log.error("Error refreshing connections from project '" + project.getId() + "'", lastError); + } return sessionProject; } - public void refreshConnections() { - - // Add all provided datasources to the session - List connList = new ArrayList<>(); - for (DBPProject project : getWorkspace().getProjects()) { - DBPDataSourceRegistry registry = project.getDataSourceRegistry(); - - for (DBPDataSourceContainer ds : registry.getDataSources()) { - connList.add(new WebConnectionInfo(this, ds)); - } - Throwable lastError = registry.getLastError(); - if (lastError != null) { - addSessionError(lastError); - log.error("Error refreshing connections from project '" + project.getId() + "'", lastError); - } - } - - // Add all provided datasources to the session - synchronized (connections) { - connections.clear(); - for (WebConnectionInfo connectionInfo : connList) { - connections.put(connectionInfo.getId(), connectionInfo); - } - } - } - public void filterAccessibleConnections(List connections) { connections.removeIf(c -> !isDataSourceAccessible(c.getDataSourceContainer())); } @@ -980,20 +956,41 @@ public class WebSession extends BaseWebSession return getWorkspace().getProjects(); } - public void addSessionProject(WebProjectImpl project) { + public void addSessionProject(@NotNull WebProjectImpl project) { getWorkspace().addProject(project); if (navigatorModel != null) { navigatorModel.getRoot().addProject(project, false); } } - public void deleteSessionProject(DBPProject project) { + public void deleteSessionProject(@Nullable WebProjectImpl project) { getWorkspace().remoteProject((WebProjectImpl) project); if (navigatorModel != null) { navigatorModel.getRoot().removeProject(project); } } + @Override + public void addSessionProject(@NotNull String projectId) throws DBException { + super.addSessionProject(projectId); + var rmProject = getRmController().getProject(projectId, false, false); + createWebProject(rmProject); + } + + @Override + public void removeSessionProject(@Nullable String projectId) throws DBException { + super.removeSessionProject(projectId); + var project = getProjectById(projectId); + if (project == null) { + return; + } + deleteSessionProject(project); + var projectConnections = project.getDataSourceRegistry().getDataSources(); + for (DBPDataSourceContainer c : projectConnections) { + removeConnection(new WebConnectionInfo(this, c)); + } + } + @Property public boolean isValid() { return getSessionActiveTimeLeft() > 0; diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionEventsFilter.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionEventsFilter.java index 67dc60e211..f0e9c3246b 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionEventsFilter.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionEventsFilter.java @@ -18,7 +18,7 @@ package io.cloudbeaver.model.session; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; -import org.jkiss.dbeaver.model.websocket.event.WSAbstractProjectEvent; +import org.jkiss.dbeaver.model.websocket.event.WSProjectResourceEvent; import org.jkiss.dbeaver.model.websocket.event.WSEvent; import java.util.Set; @@ -55,8 +55,8 @@ public class WebSessionEventsFilter { return false; } - if (!subscribedProjectIds.isEmpty() && event instanceof WSAbstractProjectEvent) { - var projectEvent = (WSAbstractProjectEvent) event; + if (!subscribedProjectIds.isEmpty() && event instanceof WSProjectResourceEvent) { + var projectEvent = (WSProjectResourceEvent) event; if (!subscribedProjectIds.contains(projectEvent.getProjectId())) { return false; } diff --git a/server/bundles/io.cloudbeaver.server/plugin.xml b/server/bundles/io.cloudbeaver.server/plugin.xml index 30c3e1dd98..25d9e051d7 100644 --- a/server/bundles/io.cloudbeaver.server/plugin.xml +++ b/server/bundles/io.cloudbeaver.server/plugin.xml @@ -49,6 +49,8 @@ + + diff --git a/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls index 5811fefb1a..09acd7d384 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls @@ -16,9 +16,12 @@ enum CBServerEventId { cb_datasource_folder_updated, cb_datasource_folder_deleted, - cb_rm_resource_created - cb_rm_resource_updated - cb_rm_resource_deleted + cb_rm_resource_created, + cb_rm_resource_updated, + cb_rm_resource_deleted, + + cb_rm_project_added, + cb_rm_project_removed } # Events sent by client @@ -86,12 +89,26 @@ type CBSessionLogEvent implements CBServerEvent { topicId: CBEventTopic } +# RM project update event +type CBProjectUpdateEvent implements CBServerEvent { + id: CBServerEventId! + topicId: CBEventTopic + projectId: String! +} + # Topic subscription event type CBTopicEvent implements CBClientEvent { id: CBClientEventId! topicId: CBEventTopic! # topic to subscribe / unsubscribe } +# Project event +type CBProjectEvent implements CBClientEvent { + id: CBClientEventId! + topicId: CBEventTopic + projectId: String! +} + # Active projects event type CBProjectsActiveEvent implements CBClientEvent { id: CBClientEventId! diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSAbstractProjectEventHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSAbstractProjectEventHandler.java new file mode 100644 index 0000000000..ab4c97fd16 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSAbstractProjectEventHandler.java @@ -0,0 +1,74 @@ +/* + * 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.events; + +import io.cloudbeaver.model.session.BaseWebSession; +import io.cloudbeaver.server.CBPlatform; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.websocket.WSEventHandler; +import org.jkiss.dbeaver.model.websocket.event.*; + +import java.util.Collection; + +/** + * Notify all active user session that project has been updated + */ +public abstract class WSAbstractProjectEventHandler implements WSEventHandler { + + @NotNull + @Override + public String getSupportedTopicId() { + return WSEventTopic.PROJECTS.getTopicId(); + } + + @Override + public void handleEvent(@NotNull WSEvent event) { + if (!getEventClass().isInstance(event)) { + return; + } + var typedEvent = getEventClass().cast(event); + Collection allSessions = CBPlatform.getInstance().getSessionManager().getAllActiveSessions(); + for (var activeUserSession : allSessions) { + if (WSWebUtils.isSessionIdEquals(activeUserSession, event.getSessionId())) { + continue; // skip events from current session + } + if (!validateEvent(activeUserSession, typedEvent)) { + getLog().debug(getSupportedTopicId() + " event '" + event.getId() + "' is not valid"); + continue; + } + getLog().debug(getSupportedTopicId() + " event '" + event.getId() + "' handled"); + updateSessionData(activeUserSession, typedEvent); + } + } + + /** + * Method to get logger from implementation to display the implementation class name in the log + */ + @NotNull + protected abstract Log getLog(); + + @NotNull + protected abstract Class getEventClass(); + + protected abstract void updateSessionData(@NotNull BaseWebSession activeUserSession, @NotNull Event event); + + protected boolean validateEvent(@NotNull BaseWebSession activeUserSession, @NotNull Event event) { + return activeUserSession.isProjectAccessible(event.getProjectId()) && + WSEventType.valueById(event.getId()) != null; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDataSourceUpdatedEventHandlerImpl.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDataSourceUpdatedEventHandlerImpl.java index 6f795f0559..2958c2a7e7 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDataSourceUpdatedEventHandlerImpl.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDataSourceUpdatedEventHandlerImpl.java @@ -20,7 +20,7 @@ import io.cloudbeaver.WebProjectImpl; import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.model.websocket.event.WSEvent; +import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.websocket.event.WSEventTopic; import org.jkiss.dbeaver.model.websocket.event.WSEventType; import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceEvent; @@ -28,34 +28,38 @@ import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceEvent; /** * Notify all active user session that datasource has been updated */ -public class WSDataSourceUpdatedEventHandlerImpl extends WSProjectUpdatedEventHandler { +public class WSDataSourceUpdatedEventHandlerImpl extends WSAbstractProjectEventHandler { + + public static final Log log = Log.getLog(WSDataSourceUpdatedEventHandlerImpl.class); + @NotNull @Override public String getSupportedTopicId() { return WSEventTopic.DATASOURCE.getTopicId(); } + @NotNull @Override - protected void updateSessionData(BaseWebSession activeUserSession, WSEvent event) { - if (!(event instanceof WSDataSourceEvent)) { - return; - } - var dsUpdateEvent = (WSDataSourceEvent) event; - if (!activeUserSession.isProjectAccessible(dsUpdateEvent.getProjectId())) { - return; - } - var eventType = WSEventType.valueById(event.getId()); - if (eventType == null) { - return; - } + protected Log getLog() { + return log; + } + + @NotNull + @Override + protected Class getEventClass() { + return WSDataSourceEvent.class; + } + + @Override + protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @NotNull WSDataSourceEvent event) { var sendEvent = true; if (activeUserSession instanceof WebSession) { var webSession = (WebSession) activeUserSession; - WebProjectImpl project = webSession.getProjectById(dsUpdateEvent.getProjectId()); + WebProjectImpl project = webSession.getProjectById(event.getProjectId()); sendEvent = webSession.updateProjectConnection( project, - dsUpdateEvent.getDataSourceIds(), - eventType + event.getDataSourceIds(), + WSEventType.valueById(event.getId()) ); } if (sendEvent) { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSFolderUpdatedEventHandlerImpl.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSFolderUpdatedEventHandlerImpl.java index 3dc6413077..fb3f1c9866 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSFolderUpdatedEventHandlerImpl.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSFolderUpdatedEventHandlerImpl.java @@ -19,7 +19,7 @@ package io.cloudbeaver.server.events; import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.model.websocket.event.WSEvent; +import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.websocket.event.WSEventTopic; import org.jkiss.dbeaver.model.websocket.event.datasource.WSDatasourceFolderEvent; import org.jkiss.utils.CommonUtils; @@ -27,31 +27,42 @@ import org.jkiss.utils.CommonUtils; /** * Notify all active user session that datasource has been updated */ -public class WSFolderUpdatedEventHandlerImpl extends WSProjectUpdatedEventHandler { +public class WSFolderUpdatedEventHandlerImpl extends WSAbstractProjectEventHandler { + + private static final Log log = Log.getLog(WSFolderUpdatedEventHandlerImpl.class); + @NotNull @Override public String getSupportedTopicId() { return WSEventTopic.DATASOURCE_FOLDER.getTopicId(); } + @NotNull @Override - protected void updateSessionData(BaseWebSession activeUserSession, WSEvent event) { - if (!(event instanceof WSDatasourceFolderEvent)) { - return; - } - var dsFolderUpdateEvent = (WSDatasourceFolderEvent) event; - boolean isProjectAccessible = activeUserSession.isProjectAccessible(dsFolderUpdateEvent.getProjectId()); - if (!isProjectAccessible || CommonUtils.isEmpty(dsFolderUpdateEvent.getNodePaths())) { - return; - } + protected Log getLog() { + return log; + } + @NotNull + @Override + protected Class getEventClass() { + return WSDatasourceFolderEvent.class; + } + + @Override + protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @NotNull WSDatasourceFolderEvent event) { if (activeUserSession instanceof WebSession) { var webSession = (WebSession) activeUserSession; - var project = webSession.getProjectById(dsFolderUpdateEvent.getProjectId()); + var project = webSession.getProjectById(event.getProjectId()); project.getDataSourceRegistry().refreshConfig(); webSession.getNavigatorModel().getRoot().getProjectNode(project).getDatabases().refreshChildren(); } activeUserSession.addSessionEvent(event); } + + @Override + protected boolean validateEvent(@NotNull BaseWebSession activeUserSession, @NotNull WSDatasourceFolderEvent event) { + return !CommonUtils.isEmpty(event.getNodePaths()) && super.validateEvent(activeUserSession, event); + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSPermissionUpdatedEventHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSPermissionUpdatedEventHandler.java new file mode 100644 index 0000000000..57eecf89bf --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSPermissionUpdatedEventHandler.java @@ -0,0 +1,110 @@ +/* + * 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.events; + +import io.cloudbeaver.model.session.BaseWebSession; +import io.cloudbeaver.server.CBPlatform; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.security.SMObjectPermissionsGrant; +import org.jkiss.dbeaver.model.security.SMObjects; +import org.jkiss.dbeaver.model.websocket.WSEventHandler; +import org.jkiss.dbeaver.model.websocket.event.WSEvent; +import org.jkiss.dbeaver.model.websocket.event.WSEventTopic; +import org.jkiss.dbeaver.model.websocket.event.WSProjectUpdateEvent; +import org.jkiss.dbeaver.model.websocket.event.permissions.WSObjectPermissionEvent; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +public class WSPermissionUpdatedEventHandler implements WSEventHandler { + private static final Log log = Log.getLog(WSPermissionUpdatedEventHandler.class); + + @NotNull + @Override + public String getSupportedTopicId() { + return WSEventTopic.PERMISSIONS.getTopicId(); + } + + @Override + public void handleEvent(@NotNull WSEvent event) { + if (!(event instanceof WSObjectPermissionEvent)) { + return; + } + var permissionEvent = (WSObjectPermissionEvent) event; + if (!SMObjects.PROJECT_OBJECT_TYPE_ID.equals(permissionEvent.getSmObjectType().getObjectType())) { + return; + } + var smController = CBPlatform.getInstance().getApplication().getSecurityController(); + try { + var projectId = permissionEvent.getObjectId(); + var permissionGrants = smController.getObjectPermissionGrants(projectId, permissionEvent.getSmObjectType()); + var subjectsWithProjectAccess = permissionGrants.stream() + .map(SMObjectPermissionsGrant::getSubjectId) + .collect(Collectors.toSet()); + + log.debug(getSupportedTopicId() + " event handled"); + + Collection allSessions = CBPlatform.getInstance().getSessionManager().getAllActiveSessions(); + for (var activeUserSession : allSessions) { + if (WSWebUtils.isSessionIdEquals(activeUserSession, event.getSessionId())) { + continue; + } + var user = activeUserSession.getUserContext().getUser(); + if (user == null) { + continue; + } + + var userSubjects = new HashSet<>(Set.of(user.getTeams())); + userSubjects.add(user.getUserId()); + + var accessibleProjectIds = activeUserSession.getUserContext().getAccessibleProjectIds(); + var isAccessibleNow = accessibleProjectIds.contains(projectId); + + var shouldBeAccessible = subjectsWithProjectAccess.stream().anyMatch(userSubjects::contains); + + if (shouldBeAccessible && !isAccessibleNow) { + // adding project to session cache + activeUserSession.addSessionProject(projectId); + activeUserSession.addSessionEvent( + WSProjectUpdateEvent.create( + event.getSessionId(), + event.getUserId(), + projectId + ) + ); + } else if (!shouldBeAccessible && isAccessibleNow) { + // removing project from session cache + activeUserSession.removeSessionProject(projectId); + activeUserSession.addSessionEvent( + WSProjectUpdateEvent.delete( + event.getSessionId(), + event.getUserId(), + projectId + ) + ); + } + } + } catch (DBException e) { + log.error("Cannot get permission grants", e); + } + + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSProjectUpdatedEventHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSProjectUpdatedEventHandler.java index ad6c9473c3..a7e8901da9 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSProjectUpdatedEventHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSProjectUpdatedEventHandler.java @@ -16,38 +16,58 @@ */ package io.cloudbeaver.server.events; +import io.cloudbeaver.DBWConstants; import io.cloudbeaver.model.session.BaseWebSession; -import io.cloudbeaver.server.CBPlatform; import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.websocket.WSEventHandler; -import org.jkiss.dbeaver.model.websocket.WSUtils; -import org.jkiss.dbeaver.model.websocket.event.WSEvent; -import org.jkiss.utils.CommonUtils; +import org.jkiss.dbeaver.model.websocket.event.WSEventTopic; +import org.jkiss.dbeaver.model.websocket.event.WSEventType; +import org.jkiss.dbeaver.model.websocket.event.WSProjectUpdateEvent; -import java.util.Collection; +public class WSProjectUpdatedEventHandler extends WSAbstractProjectEventHandler { -/** - * Notify all active user session that project has been updated - */ -public abstract class WSProjectUpdatedEventHandler implements WSEventHandler { private static final Log log = Log.getLog(WSProjectUpdatedEventHandler.class); @NotNull @Override - public abstract String getSupportedTopicId(); + public String getSupportedTopicId() { + return WSEventTopic.PROJECTS.getTopicId(); + } + + @NotNull + @Override + protected Log getLog() { + return log; + } + + @NotNull + @Override + protected Class getEventClass() { + return WSProjectUpdateEvent.class; + } @Override - public void handleEvent(@NotNull WSEvent event) { - log.debug(getSupportedTopicId() + " event handled"); - Collection allSessions = CBPlatform.getInstance().getSessionManager().getAllActiveSessions(); - for (var activeUserSession : allSessions) { - if (WSWebUtils.isSessionIdEquals(activeUserSession, event.getSessionId())) { - continue; + protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @NotNull WSProjectUpdateEvent event) { + var eventId = event.getId(); + var projectId = event.getProjectId(); + try { + if (eventId.equals(WSEventType.RM_PROJECT_ADDED.getEventId())) { + activeUserSession.addSessionProject(projectId); + log.info("Project '" + projectId + "' added to '" + activeUserSession.getSessionId() + "' session"); + } else if (eventId.equals(WSEventType.RM_PROJECT_REMOVED.getEventId())) { + activeUserSession.removeSessionProject(projectId); + log.info("Project '" + projectId + "' removed from '" + activeUserSession.getSessionId() + "' session"); } - updateSessionData(activeUserSession, event); + activeUserSession.addSessionEvent(event); + } catch (DBException e) { + log.warn("Failed to handle project lifecycle event", e); } } - protected abstract void updateSessionData(BaseWebSession activeUserSession, WSEvent event); + @Override + protected boolean validateEvent(@NotNull BaseWebSession activeUserSession, @NotNull WSProjectUpdateEvent event) { + return event.getId().equals(WSEventType.RM_PROJECT_REMOVED.getEventId()) || + activeUserSession.getUserContext().hasPermission(DBWConstants.PERMISSION_ADMIN); + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSRmResourceUpdatedEventHandlerImpl.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSRmResourceUpdatedEventHandlerImpl.java index ba3809d044..cb31b75b7b 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSRmResourceUpdatedEventHandlerImpl.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSRmResourceUpdatedEventHandlerImpl.java @@ -22,10 +22,10 @@ import io.cloudbeaver.WebProjectImpl; import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.rm.RMEvent; import org.jkiss.dbeaver.model.rm.RMEventManager; import org.jkiss.dbeaver.model.rm.RMResource; -import org.jkiss.dbeaver.model.websocket.event.WSEvent; import org.jkiss.dbeaver.model.websocket.event.WSEventTopic; import org.jkiss.dbeaver.model.websocket.event.WSEventType; import org.jkiss.dbeaver.model.websocket.event.resource.WSResourceUpdatedEvent; @@ -36,8 +36,9 @@ import java.util.List; /** * Notify all active user session that rm resource has been updated */ -public class WSRmResourceUpdatedEventHandlerImpl extends WSProjectUpdatedEventHandler { +public class WSRmResourceUpdatedEventHandlerImpl extends WSAbstractProjectEventHandler { + private static final Log log = Log.getLog(WSRmResourceUpdatedEventHandlerImpl.class); private static final Gson gson = new GsonBuilder().create(); @NotNull @@ -46,33 +47,31 @@ public class WSRmResourceUpdatedEventHandlerImpl extends WSProjectUpdatedEventHa return WSEventTopic.RM_SCRIPTS.getTopicId(); } + @NotNull @Override - protected void updateSessionData(BaseWebSession activeUserSession, WSEvent event) { - if (!(event instanceof WSResourceUpdatedEvent)) { - return; - } - var resourceUpdateEvent = (WSResourceUpdatedEvent) event; - String projectId = resourceUpdateEvent.getProjectId(); - if (!activeUserSession.isProjectAccessible(projectId)) { - return; - } - if (resourceUpdateEvent.getResourcePath() == null) { - return; - } - Object parsedResourcePath = resourceUpdateEvent.getResourceParsedPath(); - RMResource[] resourceParsedPath; - if (parsedResourcePath instanceof RMResource[]) { - resourceParsedPath = (RMResource[]) parsedResourcePath; - } else { - resourceParsedPath = gson.fromJson(gson.toJson(parsedResourcePath), RMResource[].class); - } - var eventType = WSEventType.valueById(resourceUpdateEvent.getId()); - if (eventType == null) { - return; - } + protected Log getLog() { + return log; + } + + @NotNull + @Override + protected Class getEventClass() { + return WSResourceUpdatedEvent.class; + } + + @Override + protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @NotNull WSResourceUpdatedEvent event) { if (activeUserSession instanceof WebSession) { + var parsedResourcePath = event.getResourceParsedPath(); + var resourceParsedPath = parsedResourcePath instanceof RMResource[] + ? (RMResource[]) parsedResourcePath + : gson.fromJson(gson.toJson(parsedResourcePath), RMResource[].class); var webSession = (WebSession) activeUserSession; - acceptChangesInNavigatorTree(eventType, resourceParsedPath, webSession.getProjectById(projectId)); + acceptChangesInNavigatorTree( + WSEventType.valueById(event.getId()), + resourceParsedPath, + webSession.getProjectById(event.getProjectId()) + ); } activeUserSession.addSessionEvent(event); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserSecretEventHandlerImpl.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserSecretEventHandlerImpl.java index 6f4a11c18a..055153a402 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserSecretEventHandlerImpl.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserSecretEventHandlerImpl.java @@ -18,20 +18,26 @@ package io.cloudbeaver.server.events; import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.server.CBPlatform; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.websocket.WSEventHandler; import org.jkiss.dbeaver.model.websocket.event.WSEvent; import org.jkiss.dbeaver.model.websocket.event.WSEventTopic; import org.jkiss.dbeaver.model.websocket.event.WSUserSecretEvent; import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceEvent; import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty; +import java.util.Collection; import java.util.List; /** * Notify all active user session that rm resource has been updated */ -public class WSUserSecretEventHandlerImpl extends WSProjectUpdatedEventHandler { +public class WSUserSecretEventHandlerImpl implements WSEventHandler { + + private static final Log log = Log.getLog(WSUserSecretEventHandlerImpl.class); @NotNull @Override @@ -40,6 +46,16 @@ public class WSUserSecretEventHandlerImpl extends WSProjectUpdatedEventHandler { } @Override + public void handleEvent(@NotNull WSEvent event) { + Collection allSessions = CBPlatform.getInstance().getSessionManager().getAllActiveSessions(); + for (var activeUserSession : allSessions) { + if (WSWebUtils.isSessionIdEquals(activeUserSession, event.getSessionId())) { + continue; // skip events from current session + } + updateSessionData(activeUserSession, event); + } + } + protected void updateSessionData(BaseWebSession activeUserSession, WSEvent event) { if (!(event instanceof WSUserSecretEvent && activeUserSession instanceof WebSession)) { return; diff --git a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java index ce18ca9fcd..a267ec508a 100644 --- a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java +++ b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java @@ -23,16 +23,18 @@ import io.cloudbeaver.service.rm.DBWServiceRM; import io.cloudbeaver.service.rm.model.RMProjectPermissions; import io.cloudbeaver.service.rm.model.RMSubjectProjectPermissions; import io.cloudbeaver.service.security.SMUtils; +import io.cloudbeaver.utils.WebAppUtils; import io.cloudbeaver.utils.WebEventUtils; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.model.app.DBPProject; import org.jkiss.dbeaver.model.rm.RMController; import org.jkiss.dbeaver.model.rm.RMProject; import org.jkiss.dbeaver.model.rm.RMResource; import org.jkiss.dbeaver.model.security.*; import org.jkiss.dbeaver.model.websocket.WSConstants; +import org.jkiss.dbeaver.model.websocket.event.WSProjectUpdateEvent; +import org.jkiss.dbeaver.model.websocket.event.permissions.WSObjectPermissionEvent; import org.jkiss.dbeaver.model.websocket.event.resource.WSResourceProperty; import java.nio.charset.StandardCharsets; @@ -255,6 +257,9 @@ public class WebServiceRM implements DBWServiceRM { try { RMProject rmProject = getResourceController(session).createProject(name, description); session.createWebProject(rmProject); + WebAppUtils.getWebApplication().getEventController().addEvent( + WSProjectUpdateEvent.create(session.getSessionId(), session.getUserId(), rmProject.getId()) + ); return rmProject; } catch (DBException e) { throw new DBWebException("Error creating project", e); @@ -264,9 +269,12 @@ public class WebServiceRM implements DBWServiceRM { @Override public boolean deleteProject(@NotNull WebSession session, @NotNull String projectId) throws DBWebException { try { - DBPProject project = session.getProjectById(projectId); + var project = session.getProjectById(projectId); getResourceController(session).deleteProject(projectId); session.deleteSessionProject(project); + WebAppUtils.getWebApplication().getEventController().addEvent( + WSProjectUpdateEvent.delete(session.getSessionId(), session.getUserId(), projectId) + ); return true; } catch (DBException e) { throw new DBWebException("Error deleting project", e); @@ -301,12 +309,26 @@ public class WebServiceRM implements DBWServiceRM { webSession.getUserId() ); } + addProjectPermissionsUpdatedEvent(webSession, projectId); return true; } catch (Exception e) { throw new DBWebException("Error granting project permissions", e); } } + private void addProjectPermissionsUpdatedEvent( + @NotNull WebSession webSession, + @NotNull String projectId + ) { + var event = WSObjectPermissionEvent.update( + webSession.getUserContext().getSmSessionId(), + webSession.getUserId(), + SMObjects.PROJECT, + projectId + ); + webSession.getApplication().getEventController().addEvent(event); + } + @Override public boolean setSubjectProjectPermissions( @NotNull WebSession webSession, @@ -325,6 +347,7 @@ public class WebServiceRM implements DBWServiceRM { permissions, webSession.getUserId() ); + addProjectPermissionsUpdatedEvent(webSession, projectId); } return true; } catch (Exception e) { diff --git a/webapp/packages/core-projects/src/ProjectInfoEventHandler.ts b/webapp/packages/core-projects/src/ProjectInfoEventHandler.ts index bcceae92bb..928fb5fa6a 100644 --- a/webapp/packages/core-projects/src/ProjectInfoEventHandler.ts +++ b/webapp/packages/core-projects/src/ProjectInfoEventHandler.ts @@ -7,11 +7,11 @@ */ import { injectable } from '@cloudbeaver/core-di'; -import { SessionEventSource, TopicEventHandler, ISessionEvent, ClientEventId, SessionEventTopic, IBaseServerEvent, SessionEventId } from '@cloudbeaver/core-root'; -import type { CbProjectsActiveEvent } from '@cloudbeaver/core-sdk'; +import { SessionEventSource, TopicEventHandler, ISessionEvent, ClientEventId, SessionEventTopic, SessionEventId } from '@cloudbeaver/core-root'; +import type { CbProjectsActiveEvent, CbProjectUpdateEvent as IProjectInfoEvent } from '@cloudbeaver/core-sdk'; import { isArraysEqual } from '@cloudbeaver/core-utils'; -export type IProjectInfoEvent = IBaseServerEvent; +export { IProjectInfoEvent }; @injectable() export class ProjectInfoEventHandler @@ -38,7 +38,7 @@ export class ProjectInfoEventHandler this.lastActiveProjects = projectIds; } - map(event: ISessionEvent): IProjectInfoEvent { + map(event: any): IProjectInfoEvent { return event; } } diff --git a/webapp/packages/core-projects/src/ProjectsService.ts b/webapp/packages/core-projects/src/ProjectsService.ts index 4ef7f68cb1..a84767a33f 100644 --- a/webapp/packages/core-projects/src/ProjectsService.ts +++ b/webapp/packages/core-projects/src/ProjectsService.ts @@ -11,12 +11,13 @@ import { computed, makeObservable } from 'mobx'; import { UserDataService, UserInfoResource } from '@cloudbeaver/core-authentication'; import { Dependency, injectable } from '@cloudbeaver/core-di'; import { Executor, ExecutorInterrupter, IExecutor, ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; -import { CachedMapAllKey, resourceKeyList } from '@cloudbeaver/core-sdk'; +import { DataSynchronizationService, ServerEventId } from '@cloudbeaver/core-root'; +import { CachedMapAllKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-sdk'; import { NavigationService } from '@cloudbeaver/core-ui'; import { isArraysEqual } from '@cloudbeaver/core-utils'; import { activeProjectsContext } from './activeProjectsContext'; -import { ProjectInfoEventHandler } from './ProjectInfoEventHandler'; +import { IProjectInfoEvent, ProjectInfoEventHandler } from './ProjectInfoEventHandler'; import { ProjectInfo, ProjectInfoResource } from './ProjectInfoResource'; interface IActiveProjectData { @@ -95,6 +96,7 @@ export class ProjectsService extends Dependency { private readonly userInfoResource: UserInfoResource, private readonly userDataService: UserDataService, private readonly projectInfoEventHandler: ProjectInfoEventHandler, + private readonly dataSynchronizationService: DataSynchronizationService, navigationService: NavigationService ) { super(); @@ -127,6 +129,34 @@ export class ProjectsService extends Dependency { this.projectInfoEventHandler.setActiveProjects(this.activeProjects.map(project => project.id)); }); + this.projectInfoResource.onItemDelete.addHandler(async data => { + const ids = ResourceKeyUtils.toArray(data); + const wasActive = ids.some(id => this.activeProjectIds.includes(id)); + if (wasActive) { + await this.setActiveProjects(this.activeProjects.filter(project => !ids.includes(project.id))); + } + }); + + this.projectInfoEventHandler.onEvent(ServerEventId.CbRmProjectAdded, () => { + this.projectInfoResource.markOutdated(); + }, undefined, this.projectInfoResource); + + this.projectInfoEventHandler.onEvent(ServerEventId.CbRmProjectRemoved, key => { + if (this.activeProjectIds.includes(key.projectId)) { + const project = this.projectInfoResource.get(key.projectId); + + this.dataSynchronizationService + .requestSynchronization('project', project?.name ?? '') + .then(state => { + if (state) { + this.projectInfoResource.delete(key.projectId); + } + }); + } else { + this.projectInfoResource.delete(key.projectId); + } + }, undefined, this.projectInfoResource); + makeObservable(this, { userProject: computed, defaultProject: computed, diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index 65aad8fa64..70cedb7904 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -205,6 +205,18 @@ export enum CbEventTopic { CbSessionLog = 'cb_session_log' } +export interface CbProjectEvent extends CbClientEvent { + id: CbClientEventId; + projectId: Scalars['String']; + topicId?: Maybe; +} + +export interface CbProjectUpdateEvent extends CbServerEvent { + id: CbServerEventId; + projectId: Scalars['String']; + topicId?: Maybe; +} + export interface CbProjectsActiveEvent extends CbClientEvent { id: CbClientEventId; projectIds: Array; @@ -231,6 +243,8 @@ export enum CbServerEventId { CbDatasourceFolderDeleted = 'cb_datasource_folder_deleted', CbDatasourceFolderUpdated = 'cb_datasource_folder_updated', CbDatasourceUpdated = 'cb_datasource_updated', + CbRmProjectAdded = 'cb_rm_project_added', + CbRmProjectRemoved = 'cb_rm_project_removed', CbRmResourceCreated = 'cb_rm_resource_created', CbRmResourceDeleted = 'cb_rm_resource_deleted', CbRmResourceUpdated = 'cb_rm_resource_updated', diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts index 9115b74f57..b9e980d645 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts @@ -14,7 +14,7 @@ import { ConnectionInfoActiveProjectKey, ConnectionInfoResource } from '@cloudbe import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; -import { type NavNode, NavNodeInfoResource, NavTreeResource } from '@cloudbeaver/core-navigation-tree'; +import { type NavNode, NavNodeInfoResource, NavTreeResource, ROOT_NODE_PATH } from '@cloudbeaver/core-navigation-tree'; import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; import { CachedMapAllKey, ResourceKeyUtils } from '@cloudbeaver/core-sdk'; import type { IDNDData } from '@cloudbeaver/core-ui'; @@ -81,7 +81,7 @@ export interface IElementsTreeOptions { isGroup?: (node: NavNode) => boolean; customSelect?: (node: NavNode, multiple: boolean, nested: boolean) => Promise | void; beforeSelect?: (node: NavNode, multiple: boolean, nested: boolean) => Promise | void; - customSelectReset?: ()=> Promise | void; + customSelectReset?: () => Promise | void; } interface IOptions extends IElementsTreeOptions { @@ -125,6 +125,7 @@ export interface IElementsTree extends ILoadableState { export function useElementsTree(options: IOptions): IElementsTree { const projectsService = useService(ProjectsService); + const projectInfoResource = useService(ProjectInfoResource); const notificationService = useService(NotificationService); const navNodeInfoResource = useService(NavNodeInfoResource); const navTreeResource = useService(NavTreeResource); @@ -618,6 +619,11 @@ export function useElementsTree(options: IOptions): IElementsTree { }], }); + useExecutor({ + executor: projectInfoResource.onDataOutdated, + handlers: [() => navTreeResource.markOutdated(ROOT_NODE_PATH)], + }); + useExecutor({ executor: navTreeResource.onItemUpdate, handlers: [function exitFolder(key) {