From 5206680590e5fc002678c4f1f2451ca78bf3a1e7 Mon Sep 17 00:00:00 2001 From: Ainur <59531286+yagudin10@users.noreply.github.com> Date: Wed, 16 Aug 2023 14:19:31 +0200 Subject: [PATCH] CB-3769-user-role-change-event (#1880) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * CB-3825 add role changed event * CB-3825 dc use event controller for sending events to mq * CB-3769 remove fields from gql subject event type * CB-3821 support CbSubjectPermissionUpdateEvent * CB-3769 session state job do not refresh permissions * CB-3769 change locale * CB-3769 use sm subject type * CB-3821 fix event name type * CB-3825 remove core-localization package * CB-3824 change imports * CB-3825 translate all messages --------- Co-authored-by: naumov Co-authored-by: Айнур Ягудин Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com> Co-authored-by: Alexey --- .../model/session/BaseWebSession.java | 10 +++ .../cloudbeaver/model/session/WebSession.java | 2 + .../model/session/WebUserContext.java | 12 ++-- .../bundles/io.cloudbeaver.server/plugin.xml | 7 ++- .../schema/service.events.graphqls | 15 ++++- ...SObjectPermissionUpdatedEventHandler.java} | 4 +- ...SSubjectPermissionUpdatedEventHandler.java | 61 +++++++++++++++++++ .../service/session/WebSessionManager.java | 1 - .../CBEmbeddedSecurityController.java | 40 +++++++++++- .../core-localization/src/locales/en.ts | 1 + .../core-localization/src/locales/it.ts | 1 + .../core-localization/src/locales/ru.ts | 1 + .../core-localization/src/locales/zh.ts | 1 + .../src/SessionPermissionEventHandler.ts | 25 ++++++++ .../src/SessionPermissionsResource.ts | 25 +++++++- webapp/packages/core-root/src/manifest.ts | 2 + .../DataSynchronizationNotification.tsx | 2 +- 17 files changed, 194 insertions(+), 16 deletions(-) rename server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/{WSPermissionUpdatedEventHandler.java => WSObjectPermissionUpdatedEventHandler.java} (95%) create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSSubjectPermissionUpdatedEventHandler.java create mode 100644 webapp/packages/core-root/src/SessionPermissionEventHandler.ts 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 9a5cbd6de0..caf0f59f85 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 @@ -109,6 +109,16 @@ public abstract class BaseWebSession extends AbstractSessionPersistent { return userContext.refresh(smAuthInfo); } + public synchronized void refreshUserData() { + try { + userContext.refreshPermissions(); + userContext.refreshAccessibleProjects(); + } catch (DBException e) { + addSessionError(e); + log.error("Error refreshing accessible projects", e); + } + } + @NotNull @Override public SMAuthSpace getSessionSpace() { 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 e799b9f50a..33f929e40a 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 @@ -232,7 +232,9 @@ public class WebSession extends BaseWebSession return userContext.getFileController(); } + @Override public synchronized void refreshUserData() { + super.refreshUserData(); refreshSessionAuth(); initNavigatorModel(); diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java index 9195b54ef5..36b70e0b8a 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java @@ -123,14 +123,18 @@ public class WebUserContext implements SMCredentialsProvider { if (isSessionChanged) { this.smSessionId = smAuthPermissions.getSessionId(); setUser(smAuthPermissions.getUserId() == null ? null : new WebUser(securityController.getCurrentUser())); - this.accessibleProjectIds.clear(); - this.accessibleProjectIds.addAll( - Arrays.stream(rmController.listAccessibleProjects()).map(RMProject::getId).collect(Collectors.toSet()) - ); + refreshAccessibleProjects(); } return isSessionChanged; } + public synchronized void refreshAccessibleProjects() throws DBException { + this.accessibleProjectIds.clear(); + this.accessibleProjectIds.addAll( + Arrays.stream(rmController.listAccessibleProjects()).map(RMProject::getId).collect(Collectors.toSet()) + ); + } + public synchronized void refreshSMSession() throws DBException { if (smCredentials == null || refreshToken == null) { return; diff --git a/server/bundles/io.cloudbeaver.server/plugin.xml b/server/bundles/io.cloudbeaver.server/plugin.xml index a03996a09f..71dcc26bb1 100644 --- a/server/bundles/io.cloudbeaver.server/plugin.xml +++ b/server/bundles/io.cloudbeaver.server/plugin.xml @@ -58,8 +58,11 @@ - - + + + + + diff --git a/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls index 4c372ea66d..1374063aea 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls @@ -25,7 +25,10 @@ enum CBServerEventId { cb_rm_resource_deleted, cb_rm_project_added, - cb_rm_project_removed + cb_rm_project_removed, + + cb_object_permissions_updated, + cb_subject_permissions_updated } # Events sent by client @@ -43,7 +46,9 @@ enum CBEventTopic { cb_datasource, cb_datasource_folder, cb_scripts, - cb_projects + cb_projects, + cb_object_permissions, + cb_subject_permissions } # Base server event interface @@ -122,6 +127,12 @@ type CBProjectUpdateEvent implements CBServerEvent { projectId: String! } +# Subject permission update event +type CBSubjectPermissionUpdateEvent implements CBServerEvent { + id: CBServerEventId! + topicId: CBEventTopic +} + # Topic subscription event type CBTopicEvent implements CBClientEvent { id: CBClientEventId! 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/WSObjectPermissionUpdatedEventHandler.java similarity index 95% rename from server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSPermissionUpdatedEventHandler.java rename to server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSObjectPermissionUpdatedEventHandler.java index 755ca8c935..81ceecedbb 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSPermissionUpdatedEventHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSObjectPermissionUpdatedEventHandler.java @@ -30,8 +30,8 @@ import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; -public class WSPermissionUpdatedEventHandler extends WSDefaultEventHandler { - private static final Log log = Log.getLog(WSPermissionUpdatedEventHandler.class); +public class WSObjectPermissionUpdatedEventHandler extends WSDefaultEventHandler { + private static final Log log = Log.getLog(WSObjectPermissionUpdatedEventHandler.class); private volatile Set subjectsWithProjectAccess; @Override diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSSubjectPermissionUpdatedEventHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSSubjectPermissionUpdatedEventHandler.java new file mode 100644 index 0000000000..c5946411fb --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSSubjectPermissionUpdatedEventHandler.java @@ -0,0 +1,61 @@ +/* + * 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 org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.websocket.event.permissions.WSSubjectPermissionEvent; +import org.jkiss.utils.ArrayUtils; +import org.jkiss.utils.CommonUtils; + +public class WSSubjectPermissionUpdatedEventHandler extends WSDefaultEventHandler { + private static final Log log = Log.getLog(WSSubjectPermissionUpdatedEventHandler.class); + + @Override + protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @NotNull WSSubjectPermissionEvent event) { + try { + activeUserSession.getUserContext().refreshSMSession(); + } catch (DBException e) { + activeUserSession.addSessionError(e); + log.error("Error refreshing session", e); + } + activeUserSession.refreshUserData(); + super.updateSessionData(activeUserSession, event); + } + + @Override + protected boolean isAcceptableInSession(@NotNull BaseWebSession activeUserSession, @NotNull WSSubjectPermissionEvent event) { + if (!super.isAcceptableInSession(activeUserSession, event)) { + return false; + } + var user = activeUserSession.getUserContext().getUser(); + if (user == null) { + return false; + } + var subjectId = event.getSubjectId(); + switch (event.getSubjectType()) { + case user: + return CommonUtils.equalObjects(user.getUserId(), subjectId); + case team: + return ArrayUtils.containsIgnoreCase(user.getTeams(), subjectId); + default: + return false; + } + } +} 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 25f00b8b64..a550efc091 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 @@ -333,7 +333,6 @@ public class WebSessionManager { continue; } try { - session.getUserContext().refreshPermissions(); session.addSessionEvent(new WSSessionStateEvent(session.getRemainingTime(), session.isValid())); } catch (Exception e) { log.error("Failed to refresh session state: " + session.getSessionId(), e); 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 009108d417..771fca6c2d 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 @@ -51,6 +51,7 @@ 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.permissions.WSSubjectPermissionEvent; import org.jkiss.utils.ArrayUtils; import org.jkiss.utils.CommonUtils; import org.jkiss.utils.SecurityUtils; @@ -214,8 +215,17 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen } catch (SQLException e) { throw new DBCException("Error saving user teams in database", e); } + var event = WSSubjectPermissionEvent.update( + getSmSessionId(), + getUserId(), + SMSubjectType.user, + userId + ); + application.getEventController().addEvent(event); } + + @NotNull @Override public SMTeam[] getUserTeams(String userId) throws DBException { @@ -588,8 +598,17 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen } catch (SQLException e) { throw new DBCException("Error while updating user authentication role", e); } + var event = WSSubjectPermissionEvent.update( + getSmSessionId(), + getUserId(), + SMSubjectType.user, + userId + ); + application.getEventController().addEvent(event); } + + /////////////////////////////////////////// // Credentials @@ -1570,7 +1589,6 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen public SMTokens refreshSession(@NotNull String refreshToken) throws DBException { var currentUserCreds = getCurrentUserCreds(); var currentUserAccessToken = currentUserCreds.getSmAccessToken(); - String currentUserAuthRole = readTokenAuthRole(currentUserAccessToken); var smTokenInfo = readAccessTokenInfo(currentUserAccessToken); @@ -1580,7 +1598,11 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen try (var dbCon = database.openConnection()) { invalidateUserTokens(currentUserAccessToken); - return generateNewSessionToken(smTokenInfo.getSessionId(), smTokenInfo.getUserId(), currentUserAuthRole, dbCon); + return generateNewSessionToken( + smTokenInfo.getSessionId(), + smTokenInfo.getUserId(), + updateUserAuthRoleIfNeeded(smTokenInfo.getUserId(), null), + dbCon); } catch (SQLException e) { throw new DBException("Error refreshing sm session", e); } @@ -1937,7 +1959,7 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen } } - private String updateUserAuthRoleIfNeeded(@Nullable String userId, @Nullable String authRole) throws DBException { + protected String updateUserAuthRoleIfNeeded(@Nullable String userId, @Nullable String authRole) throws DBException { if (userId == null) { return null; } @@ -2574,4 +2596,16 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen return Set.of(); } } + + @Nullable + private String getSmSessionId() { + var credentials = credentialsProvider.getActiveUserCredentials(); + return credentials == null ? null : credentials.getSmSessionId(); + } + + @Nullable + private String getUserId() { + var credentials = credentialsProvider.getActiveUserCredentials(); + return credentials == null ? null : credentials.getUserId(); + } } diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts index f73b888eaa..b4d73569a7 100644 --- a/webapp/packages/core-localization/src/locales/en.ts +++ b/webapp/packages/core-localization/src/locales/en.ts @@ -113,5 +113,6 @@ export default [ ['app_root_server_node_changed_title', 'The application was relaunched'], ['app_root_server_node_changed_message', 'The application was relaunched. Please, reload the page.'], ['app_root_quota_exceeded', 'Quota exceeded'], + ['app_root_event_permissions_changed_message', 'Your permissions have been changed'], ['core_eventsLog_dbeaverErrorDetails', 'Error details'], ]; diff --git a/webapp/packages/core-localization/src/locales/it.ts b/webapp/packages/core-localization/src/locales/it.ts index e72ea858cd..95655bf649 100644 --- a/webapp/packages/core-localization/src/locales/it.ts +++ b/webapp/packages/core-localization/src/locales/it.ts @@ -92,5 +92,6 @@ export default [ ['app_root_session_expire_warning_button', 'Sono qua'], ['app_root_session_expired_title', 'Sessione scaduta'], ['app_root_session_expired_message', 'La sessione è scaduta. Vuoi ricaricare?'], + ['app_root_event_permissions_changed_message', 'Your permissions have been changed'], ['core_eventsLog_dbeaverErrorDetails', "Dettagli dell'errore"], ]; diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index c421686d28..ddc95bf08f 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -109,5 +109,6 @@ export default [ ['app_root_server_node_changed_title', 'Приложение было перезапущено'], ['app_root_server_node_changed_message', 'Приложение было перезапущено. Пожалуйста, перезагрузите страницу.'], ['app_root_quota_exceeded', 'Квота превышена'], + ['app_root_event_permissions_changed_message', 'Ваши права доступа были изменены'], ['core_eventsLog_dbeaverErrorDetails', 'Детали ошибки'], ]; diff --git a/webapp/packages/core-localization/src/locales/zh.ts b/webapp/packages/core-localization/src/locales/zh.ts index 706d8ce268..40e31fabe4 100644 --- a/webapp/packages/core-localization/src/locales/zh.ts +++ b/webapp/packages/core-localization/src/locales/zh.ts @@ -107,5 +107,6 @@ export default [ ['app_root_session_expired_title', '会话已过期'], ['app_root_session_expired_message', '会话已过期。您想重新加载吗?'], ['app_root_quota_exceeded', '超出配额'], + ['app_root_event_permissions_changed_message', 'Your permissions have been changed'], ['core_eventsLog_dbeaverErrorDetails', '错误详情'], ]; diff --git a/webapp/packages/core-root/src/SessionPermissionEventHandler.ts b/webapp/packages/core-root/src/SessionPermissionEventHandler.ts new file mode 100644 index 0000000000..524a296ef2 --- /dev/null +++ b/webapp/packages/core-root/src/SessionPermissionEventHandler.ts @@ -0,0 +1,25 @@ +/* + * 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 { CbSubjectPermissionUpdateEvent as ISessionPermissionEvent } from '@cloudbeaver/core-sdk'; + +import { TopicEventHandler } from './ServerEventEmitter/TopicEventHandler'; +import { ISessionEvent, SessionEventId, SessionEventSource, SessionEventTopic } from './SessionEventSource'; + +export { ISessionPermissionEvent }; + +@injectable() +export class SessionPermissionEventHandler extends TopicEventHandler { + constructor(sessionEventSource: SessionEventSource) { + super(SessionEventTopic.CbSubjectPermissions, sessionEventSource); + } + + map(event: any): ISessionPermissionEvent { + return event; + } +} diff --git a/webapp/packages/core-root/src/SessionPermissionsResource.ts b/webapp/packages/core-root/src/SessionPermissionsResource.ts index 1c3623795a..9c593ba8e3 100644 --- a/webapp/packages/core-root/src/SessionPermissionsResource.ts +++ b/webapp/packages/core-root/src/SessionPermissionsResource.ts @@ -9,11 +9,21 @@ import { injectable } from '@cloudbeaver/core-di'; import { ExecutorInterrupter } from '@cloudbeaver/core-executor'; import { CachedDataResource, CachedResource, GraphQLService } from '@cloudbeaver/core-sdk'; +import { DataSynchronizationService } from './DataSynchronization/DataSynchronizationService'; import { SessionDataResource } from './SessionDataResource'; +import { ServerEventId } from './SessionEventSource'; +import { ISessionPermissionEvent, SessionPermissionEventHandler } from './SessionPermissionEventHandler'; +import { SessionResource } from './SessionResource'; @injectable() export class SessionPermissionsResource extends CachedDataResource> { - constructor(private readonly graphQLService: GraphQLService, sessionDataResource: SessionDataResource) { + constructor( + private readonly graphQLService: GraphQLService, + private readonly dataSynchronizationService: DataSynchronizationService, + sessionDataResource: SessionDataResource, + sessionResource: SessionResource, + sessionPermissionEventHandler: SessionPermissionEventHandler, + ) { super(() => new Set()); this.sync( @@ -21,6 +31,19 @@ export class SessionPermissionsResource extends CachedDataResource> () => {}, () => {}, ); + + sessionPermissionEventHandler.onEvent( + ServerEventId.CbSubjectPermissionsUpdated, + () => { + this.dataSynchronizationService.requestSynchronization('permissions', 'app_root_event_permissions_changed_message').then(state => { + if (state) { + sessionResource.markOutdated(); + } + }); + }, + undefined, + sessionResource, + ); } require(resource: CachedResource, ...permissions: string[]): this { diff --git a/webapp/packages/core-root/src/manifest.ts b/webapp/packages/core-root/src/manifest.ts index ed8d489c2d..bc444e8614 100644 --- a/webapp/packages/core-root/src/manifest.ts +++ b/webapp/packages/core-root/src/manifest.ts @@ -23,6 +23,7 @@ import { SessionEventSource } from './SessionEventSource'; import { SessionExpireEventService } from './SessionExpireEventService'; import { SessionExpireService } from './SessionExpireService'; import { SessionInfoEventHandler } from './SessionInfoEventHandler'; +import { SessionPermissionEventHandler } from './SessionPermissionEventHandler'; import { SessionPermissionsResource } from './SessionPermissionsResource'; import { SessionResource } from './SessionResource'; import { SessionService } from './SessionService'; @@ -56,5 +57,6 @@ export const manifest: PluginManifest = { SessionEventSource, SessionInfoEventHandler, DataSynchronizationService, + SessionPermissionEventHandler, ], }; diff --git a/webapp/packages/plugin-root/src/DataSynchronization/DataSynchronizationNotification.tsx b/webapp/packages/plugin-root/src/DataSynchronization/DataSynchronizationNotification.tsx index 4da53d8df0..e0af00d675 100644 --- a/webapp/packages/plugin-root/src/DataSynchronization/DataSynchronizationNotification.tsx +++ b/webapp/packages/plugin-root/src/DataSynchronization/DataSynchronizationNotification.tsx @@ -59,7 +59,7 @@ export const DataSynchronizationNotification = observer {groups.map(messages => { - const message = uniq(messages.map(m => m.message)).join('\n'); + const message = uniq(messages.map(m => translate(m.message))).join('\n'); return
{message}
; })}