diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSObjectPermissionUpdatedEventHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSObjectPermissionUpdatedEventHandler.java index 729a60eecd..3076c5f1ae 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSObjectPermissionUpdatedEventHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSObjectPermissionUpdatedEventHandler.java @@ -18,21 +18,18 @@ import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.server.CBPlatform; import io.cloudbeaver.utils.WebAppUtils; 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.SMObjectType; +import org.jkiss.dbeaver.model.websocket.event.WSEventType; import org.jkiss.dbeaver.model.websocket.event.WSProjectUpdateEvent; import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceEvent; import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty; import org.jkiss.dbeaver.model.websocket.event.permissions.WSObjectPermissionEvent; -import java.util.HashSet; import java.util.List; -import java.util.Set; public class WSObjectPermissionUpdatedEventHandler extends WSDefaultEventHandler { private static final Log log = Log.getLog(WSObjectPermissionUpdatedEventHandler.class); @@ -44,24 +41,16 @@ protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @Not if (event.getSmObjectType() == SMObjectType.datasource && !(activeUserSession instanceof WebSession)) { return; } - var user = activeUserSession.getUserContext().getUser(); var objectId = event.getObjectId(); - var userSubjects = new HashSet<>(Set.of(user.getTeams())); - userSubjects.add(user.getUserId()); - - var smController = CBPlatform.getInstance().getApplication().getSecurityController(); - var shouldBeAccessible = smController.getObjectPermissionGrants(event.getObjectId(), event.getSmObjectType()) - .stream() - .map(SMObjectPermissionsGrant::getSubjectId) - .anyMatch(userSubjects::contains); boolean isAccessibleNow; switch (event.getSmObjectType()) { case project: - var accessibleProjectIds = activeUserSession.getUserContext().getAccessibleProjectIds(); - isAccessibleNow = accessibleProjectIds.contains(objectId); - if (shouldBeAccessible && !isAccessibleNow) { - // adding project to session cache + if (WSEventType.OBJECT_PERMISSIONS_UPDATED.getEventId().equals(event.getId())) { + var accessibleProjectIds = activeUserSession.getUserContext().getAccessibleProjectIds(); + if (accessibleProjectIds.contains(event.getObjectId())) { + return; + } activeUserSession.addSessionProject(objectId); activeUserSession.addSessionEvent( WSProjectUpdateEvent.create( @@ -70,8 +59,7 @@ protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @Not objectId ) ); - } else if (!shouldBeAccessible && isAccessibleNow) { - // removing project from session cache + } else if (WSEventType.OBJECT_PERMISSIONS_DELETED.getEventId().equals(event.getId())) { activeUserSession.removeSessionProject(objectId); activeUserSession.addSessionEvent( WSProjectUpdateEvent.delete( @@ -80,19 +68,23 @@ protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @Not objectId ) ); - }; + } break; case datasource: var webSession = (WebSession) activeUserSession; + var dataSources = List.of(objectId); + var project = webSession.getProjectById(WebAppUtils.getGlobalProjectId()); if (project == null) { log.error("Project " + WebAppUtils.getGlobalProjectId() + " is not found in session " + activeUserSession.getSessionId()); return; } - isAccessibleNow = webSession.findWebConnectionInfo(objectId) != null; - var dataSources = List.of(objectId); - if (shouldBeAccessible && !isAccessibleNow) { + if (WSEventType.OBJECT_PERMISSIONS_UPDATED.getEventId().equals(event.getId())) { + isAccessibleNow = webSession.findWebConnectionInfo(objectId) != null; + if (isAccessibleNow) { + return; + } webSession.addAccessibleConnectionToCache(objectId); webSession.addSessionEvent( WSDataSourceEvent.create( @@ -103,7 +95,7 @@ protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @Not WSDataSourceProperty.CONFIGURATION ) ); - } else if (!shouldBeAccessible && isAccessibleNow) { + } else if (WSEventType.OBJECT_PERMISSIONS_DELETED.getEventId().equals(event.getId())) { webSession.removeAccessibleConnectionFromCache(objectId); webSession.addSessionEvent( WSDataSourceEvent.delete( diff --git a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls index 67e8da9fa6..a0978f5a02 100644 --- a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls +++ b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls @@ -157,9 +157,15 @@ extend type Query { # Permissions getConnectionSubjectAccess(projectId: ID!, connectionId: ID): [AdminConnectionGrantInfo!]! + @deprecated(reason: "23.2.2") setConnectionSubjectAccess(projectId: ID!, connectionId: ID!, subjects: [ID!]!): Boolean + @since(version: "23.2.2") + addConnectionsAccess(projectId: ID!, connectionIds: [ID!]!, subjects: [ID!]!): Boolean + @since(version: "23.2.2") + deleteConnectionsAccess(projectId: ID!, connectionIds: [ID!]!, subjects: [ID!]!): Boolean getSubjectConnectionAccess(subjectId: ID!): [AdminConnectionGrantInfo!]! + @deprecated(reason: "23.2.2") setSubjectConnectionAccess(subjectId: ID!, connections: [ID!]!): Boolean #### Feature sets diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/DBWServiceAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/DBWServiceAdmin.java index 05bc52993e..4759ec8891 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/DBWServiceAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/DBWServiceAdmin.java @@ -147,6 +147,7 @@ SMDataSourceGrant[] getConnectionSubjectAccess( @Nullable String projectId, String connectionId) throws DBWebException; + @Deprecated @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) boolean setConnectionSubjectAccess( @NotNull WebSession webSession, @@ -154,6 +155,22 @@ boolean setConnectionSubjectAccess( @NotNull String connectionId, @NotNull List subjects) throws DBWebException; + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) + boolean addConnectionsAccess( + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull List connectionIds, + @NotNull List subjects + ) throws DBWebException; + + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) + boolean deleteConnectionsAccess( + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull List connectionIds, + @NotNull List subjects + ) throws DBWebException; + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) SMDataSourceGrant[] getSubjectConnectionAccess(@NotNull WebSession webSession, @NotNull String subjectId) throws DBWebException; diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java index 551c292581..05af27d11f 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java @@ -95,27 +95,41 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { env -> getService(env).revokeUserTeam(getWebSession(env), env.getArgument("userId"), env.getArgument("teamId"))) .dataFetcher("setSubjectPermissions", env -> getService(env).setSubjectPermissions(getWebSession(env), env.getArgument("subjectId"), env.getArgument("permissions"))) - .dataFetcher("setUserCredentials", - env -> getService(env).setUserCredentials(getWebSession(env), env.getArgument("userId"), env.getArgument("providerId"), env.getArgument("credentials"))) - .dataFetcher("deleteUserCredentials", - env -> getService(env).deleteUserCredentials(getWebSession(env), env.getArgument("userId"), env.getArgument("providerId"))) - .dataFetcher("enableUser", - env -> getService(env).enableUser(getWebSession(env), env.getArgument("userId"), env.getArgument("enabled"))) - .dataFetcher("setUserAuthRole", - env -> getService(env).setUserAuthRole(getWebSession(env), env.getArgument("userId"), env.getArgument("authRole"))) - .dataFetcher("searchConnections", env -> getService(env).searchConnections(getWebSession(env), env.getArgument("hostNames"))) - - .dataFetcher("getConnectionSubjectAccess", - env -> getService(env).getConnectionSubjectAccess( - getWebSession(env), - getProjectReference(env), - env.getArgument("connectionId"))) - .dataFetcher("setConnectionSubjectAccess", - env -> getService(env).setConnectionSubjectAccess( - getWebSession(env), - getProjectReference(env), - env.getArgument("connectionId"), - env.getArgument("subjects"))) + .dataFetcher("setUserCredentials", + env -> getService(env).setUserCredentials(getWebSession(env), + env.getArgument("userId"), + env.getArgument("providerId"), + env.getArgument("credentials"))) + .dataFetcher("deleteUserCredentials", + env -> getService(env).deleteUserCredentials(getWebSession(env), env.getArgument("userId"), env.getArgument("providerId"))) + .dataFetcher("enableUser", + env -> getService(env).enableUser(getWebSession(env), env.getArgument("userId"), env.getArgument("enabled"))) + .dataFetcher("setUserAuthRole", + env -> getService(env).setUserAuthRole(getWebSession(env), env.getArgument("userId"), env.getArgument("authRole"))) + .dataFetcher("searchConnections", env -> getService(env).searchConnections(getWebSession(env), env.getArgument("hostNames"))) + .dataFetcher("getConnectionSubjectAccess", + env -> getService(env).getConnectionSubjectAccess( + getWebSession(env), + getProjectReference(env), + env.getArgument("connectionId"))) + .dataFetcher("setConnectionSubjectAccess", + env -> getService(env).setConnectionSubjectAccess( + getWebSession(env), + getProjectReference(env), + env.getArgument("connectionId"), + env.getArgument("subjects"))) + .dataFetcher("addConnectionsAccess", + env -> getService(env).addConnectionsAccess( + getWebSession(env), + getProjectReference(env), + env.getArgument("connectionIds"), + env.getArgument("subjects"))) + .dataFetcher("deleteConnectionsAccess", + env -> getService(env).deleteConnectionsAccess( + getWebSession(env), + getProjectReference(env), + env.getArgument("connectionIds"), + env.getArgument("subjects"))) .dataFetcher("getSubjectConnectionAccess", env -> getService(env).getSubjectConnectionAccess(getWebSession(env), env.getArgument("subjectId"))) diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java index 039fed41f4..9a903f9364 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java @@ -610,14 +610,7 @@ public boolean setConnectionSubjectAccess( @NotNull String connectionId, @NotNull List subjects ) throws DBWebException { - DBPProject globalProject = webSession.getProjectById(projectId); - if (!WebServiceUtils.isGlobalProject(globalProject)) { - throw new DBWebException("Project '" + projectId + "'is not global"); - } - DBPDataSourceContainer dataSource = getDataSourceRegistry(webSession, projectId).getDataSource(connectionId); - if (dataSource == null) { - throw new DBWebException("Connection '" + connectionId + "' not found"); - } + validateThatConnectionGlobal(webSession, projectId, List.of(connectionId)); WebUser grantor = webSession.getUser(); if (grantor == null) { throw new DBWebException("Cannot grant connection access in anonymous mode"); @@ -635,6 +628,72 @@ public boolean setConnectionSubjectAccess( return true; } + void validateThatConnectionGlobal(WebSession webSession, String projectId, Collection connectionIds) throws DBWebException { + DBPProject globalProject = webSession.getProjectById(projectId); + if (!WebServiceUtils.isGlobalProject(globalProject)) { + throw new DBWebException("Project '" + projectId + "'is not global"); + } + for (String connectionId : connectionIds) { + DBPDataSourceContainer dataSource = getDataSourceRegistry(webSession, projectId).getDataSource(connectionId); + if (dataSource == null) { + throw new DBWebException("Connection '" + connectionId + "' not found"); + } + } + } + + @Override + public boolean addConnectionsAccess( + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull List connectionIds, + @NotNull List subjects + ) throws DBWebException { + validateThatConnectionGlobal(webSession, projectId, connectionIds); + WebUser grantor = webSession.getUser(); + if (grantor == null) { + throw new DBWebException("Cannot grant connection access in anonymous mode"); + } + try { + var adminSM = webSession.getAdminSecurityController(); + adminSM.addObjectPermissions( + new HashSet<>(connectionIds), + SMObjectType.datasource, + new HashSet<>(subjects), + Set.of(SMConstants.DATA_SOURCE_ACCESS_PERMISSION), + grantor.getUserId() + ); + } catch (DBException e) { + throw new DBWebException("Error adding connection subject access", e); + } + return true; + } + + @Override + public boolean deleteConnectionsAccess( + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull List connectionIds, + @NotNull List subjects + ) throws DBWebException { + validateThatConnectionGlobal(webSession, projectId, connectionIds); + WebUser grantor = webSession.getUser(); + if (grantor == null) { + throw new DBWebException("Cannot grant connection access in anonymous mode"); + } + try { + var adminSM = webSession.getAdminSecurityController(); + adminSM.deleteObjectPermissions( + new HashSet<>(connectionIds), + SMObjectType.datasource, + new HashSet<>(subjects), + Set.of(SMConstants.DATA_SOURCE_ACCESS_PERMISSION) + ); + } catch (DBException e) { + throw new DBWebException("Error adding connection subject access", e); + } + return true; + } + @Override public SMDataSourceGrant[] getSubjectConnectionAccess(@NotNull WebSession webSession, @NotNull String subjectId) throws DBWebException { try { diff --git a/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls b/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls index 79589bb1fa..d06e8ce007 100644 --- a/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls +++ b/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls @@ -98,9 +98,17 @@ extend type Mutation { rmDeleteProject(projectId: ID!): Boolean! + @deprecated rmSetProjectPermissions(projectId: String!, permissions: [RMSubjectProjectPermissions!]!): Boolean! + @deprecated rmSetSubjectProjectPermissions(subjectId: String!, permissions: [RMProjectPermissions!]!): Boolean! + @since(version: "23.2.2") + rmAddProjectsPermissions(projectIds: [ID!]!, subjectIds: [ID!]!, permissions:[String!]! ): Boolean + @since(version: "23.2.2") + rmDeleteProjectsPermissions(projectIds: [ID!]!, subjectIds: [ID!]!, permissions:[String!]!): Boolean + + rmSetResourceProperty(projectId: String!, resourcePath: String!, name: ID!, value: String): Boolean! } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/DBWServiceRM.java b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/DBWServiceRM.java index 355fe25f12..f52a60ec7e 100644 --- a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/DBWServiceRM.java +++ b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/DBWServiceRM.java @@ -136,6 +136,7 @@ boolean deleteProject( @WebAction(requirePermissions = {RMConstants.PERMISSION_RM_ADMIN}) List listProjectPermissions() throws DBWebException; + @Deprecated @WebProjectAction( requireProjectPermissions = RMConstants.PERMISSION_PROJECT_ADMIN ) @@ -145,6 +146,7 @@ boolean setProjectPermissions( @NotNull RMSubjectProjectPermissions projectPermissions ) throws DBWebException; + @Deprecated @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) boolean setSubjectProjectPermissions( @NotNull WebSession webSession, @@ -152,6 +154,22 @@ boolean setSubjectProjectPermissions( @NotNull RMProjectPermissions projectPermissions ) throws DBWebException; + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) + boolean deleteProjectsPermissions( + @NotNull WebSession webSession, + @NotNull List projectIds, + @NotNull List subjectIds, + @NotNull List permissions + ) throws DBWebException; + + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) + boolean addProjectsPermissions( + @NotNull WebSession webSession, + @NotNull List projectIds, + @NotNull List subjectIds, + @NotNull List permissions + ) throws DBWebException; + @WebProjectAction( requireProjectPermissions = RMConstants.PERMISSION_PROJECT_ADMIN ) diff --git a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/WebServiceBindingRM.java b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/WebServiceBindingRM.java index fac21fac06..1909e3126b 100644 --- a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/WebServiceBindingRM.java +++ b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/WebServiceBindingRM.java @@ -112,6 +112,18 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { env.getArgument("subjectId"), new RMProjectPermissions(env.getArgument("permissions")) )) + .dataFetcher("rmAddProjectsPermissions", env -> getService(env).addProjectsPermissions( + getWebSession(env), + env.getArgument("projectIds"), + env.getArgument("subjectIds"), + env.getArgument("permissions") + )) + .dataFetcher("rmDeleteProjectsPermissions", env -> getService(env).addProjectsPermissions( + getWebSession(env), + env.getArgument("projectIds"), + env.getArgument("subjectIds"), + env.getArgument("permissions") + )) ; } } 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 96ade14e07..0f88521937 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 @@ -38,6 +38,7 @@ import org.jkiss.dbeaver.model.websocket.event.resource.WSResourceProperty; import java.nio.charset.StandardCharsets; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -349,6 +350,49 @@ public boolean setSubjectProjectPermissions( } } + @Override + public boolean deleteProjectsPermissions( + @NotNull WebSession webSession, + @NotNull List projectIds, + @NotNull List subjectIds, + @NotNull List permissions + ) throws DBWebException { + try { + SMAdminController smAdminController = webSession.getAdminSecurityController(); + smAdminController.deleteObjectPermissions( + new HashSet<>(projectIds), + SMObjectType.project, + new HashSet<>(subjectIds), + new HashSet<>(permissions) + ); + return true; + } catch (Exception e) { + throw new DBWebException("Error deleting project permissions", e); + } + } + + @Override + public boolean addProjectsPermissions( + @NotNull WebSession webSession, + @NotNull List projectIds, + @NotNull List subjectIds, + @NotNull List permissions + ) throws DBWebException { + try { + SMAdminController smAdminController = webSession.getAdminSecurityController(); + smAdminController.addObjectPermissions( + new HashSet<>(projectIds), + SMObjectType.project, + new HashSet<>(subjectIds), + new HashSet<>(permissions), + webSession.getUserId() + ); + return true; + } catch (Exception e) { + throw new DBWebException("Error adding project permissions", e); + } + } + @Override public List listProjectGrantedPermissions(@NotNull WebSession webSession, @NotNull String projectId 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 875de6b57d..ec5407dc53 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 @@ -2265,6 +2265,90 @@ public void setObjectPermissions( } + @Override + public void addObjectPermissions( + @NotNull Set objectIds, + @NotNull SMObjectType objectType, + @NotNull Set subjectIds, + @NotNull Set permissions, + @NotNull String grantor + ) throws DBException { + if (CommonUtils.isEmpty(objectIds) || CommonUtils.isEmpty(subjectIds) || CommonUtils.isEmpty(permissions)) { + return; + } + Set filteredSubjects = getFilteredSubjects(subjectIds); + try (Connection dbCon = database.openConnection(); + JDBCTransaction txn = new JDBCTransaction(dbCon); + PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames( + "INSERT INTO {table_prefix}CB_OBJECT_PERMISSIONS" + + "(OBJECT_ID,OBJECT_TYPE,GRANT_TIME,GRANTED_BY,SUBJECT_ID,PERMISSION) " + + "VALUES(?,?,?,?,?,?)")) + ) { + for (String objectId : objectIds) { + dbStat.setString(1, objectId); + dbStat.setString(2, objectType.name()); + dbStat.setTimestamp(3, new Timestamp(System.currentTimeMillis())); + dbStat.setString(4, grantor); + for (String subjectId : subjectIds) { + if (!filteredSubjects.contains(subjectId)) { + log.error("Subject '" + subjectId + "' is not found in database"); + continue; + } + dbStat.setString(5, subjectId); + for (String permission : permissions) { + dbStat.setString(6, permission); + dbStat.execute(); + } + } + txn.commit(); + } + addObjectPermissionsUpdateEvent(objectIds, objectType); + } catch (SQLException e) { + throw new DBCException("Error granting object permissions", e); + } + } + + @Override + public void deleteObjectPermissions( + @NotNull Set objectIds, + @NotNull SMObjectType objectType, + @NotNull Set subjectIds, + @NotNull Set permissions + ) throws DBException { + if (CommonUtils.isEmpty(objectIds) || CommonUtils.isEmpty(subjectIds) || CommonUtils.isEmpty(permissions)) { + return; + } + String sql = "DELETE FROM {table_prefix}CB_OBJECT_PERMISSIONS WHERE " + + "OBJECT_TYPE=?" + + " AND " + + "OBJECT_ID IN (" + SQLUtils.generateParamList(objectIds.size()) + ")" + + " AND " + + "SUBJECT_ID IN (" + SQLUtils.generateParamList(subjectIds.size()) + ")" + + " AND " + + "PERMISSION IN (" + SQLUtils.generateParamList(permissions.size()) + ")"; + + try ( + Connection dbCon = database.openConnection(); + PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames(sql)) + ) { + int index = 1; + dbStat.setString(index++, objectType.name()); + for (String objectId : objectIds) { + dbStat.setString(index++, objectId); + } + for (String subjectId : subjectIds) { + dbStat.setString(index++, subjectId); + } + for (String permission : permissions) { + dbStat.setString(index++, permission); + } + dbStat.execute(); + addObjectPermissionsDeleteEvent(objectIds, objectType); + } catch (SQLException e) { + throw new DBCException("Error granting object permissions", e); + } + } + private void addSubjectPermissionsUpdateEvent(@NotNull String subjectId, @Nullable SMSubjectType subjectType) { if (subjectType == null) { @@ -2295,6 +2379,18 @@ private void addObjectPermissionsUpdateEvent(@NotNull Set objectIds, @No } } + private void addObjectPermissionsDeleteEvent(@NotNull Set objectIds, @NotNull SMObjectType objectType) { + for (var objectId : objectIds) { + var event = WSObjectPermissionEvent.delete( + getSmSessionId(), + getUserId(), + objectType, + objectId + ); + application.getEventController().addEvent(event); + } + } + @Override public void deleteAllObjectPermissions(@NotNull String objectId, @NotNull SMObjectType objectType) throws DBException { try (Connection dbCon = database.openConnection()) { diff --git a/webapp/packages/core-authentication/src/UsersResource.ts b/webapp/packages/core-authentication/src/UsersResource.ts index fc53b3067b..ba1d6e8f62 100644 --- a/webapp/packages/core-authentication/src/UsersResource.ts +++ b/webapp/packages/core-authentication/src/UsersResource.ts @@ -111,8 +111,20 @@ export class UsersResource extends CachedMapResource { - await this.graphQLService.sdk.setConnections({ userId, connections }); + async addConnectionsAccess(projectId: string, userId: string, connectionIds: string[]): Promise { + await this.graphQLService.sdk.addConnectionsAccess({ + projectId, + connectionIds, + subjects: [userId], + }); + } + + async deleteConnectionsAccess(projectId: string, userId: string, connectionIds: string[]): Promise { + await this.graphQLService.sdk.deleteConnectionsAccess({ + projectId, + connectionIds, + subjects: [userId], + }); } async setMetaParameters(userId: string, parameters: Record): Promise { diff --git a/webapp/packages/core-connections/src/ConnectionInfoResource.ts b/webapp/packages/core-connections/src/ConnectionInfoResource.ts index 22449f57e1..df93e6ada4 100644 --- a/webapp/packages/core-connections/src/ConnectionInfoResource.ts +++ b/webapp/packages/core-connections/src/ConnectionInfoResource.ts @@ -361,10 +361,18 @@ export class ConnectionInfoResource extends CachedMapResource { - await this.graphQLService.sdk.setConnectionAccess({ + async addConnectionsAccess(connectionKey: IConnectionInfoParams, subjects: string[]): Promise { + await this.graphQLService.sdk.addConnectionsAccess({ projectId: connectionKey.projectId, - connectionId: connectionKey.connectionId, + connectionIds: [connectionKey.connectionId], + subjects, + }); + } + + async deleteConnectionsAccess(connectionKey: IConnectionInfoParams, subjects: string[]): Promise { + await this.graphQLService.sdk.deleteConnectionsAccess({ + projectId: connectionKey.projectId, + connectionIds: [connectionKey.connectionId], subjects, }); } diff --git a/webapp/packages/core-resource-manager/src/SharedProjectsResource.ts b/webapp/packages/core-resource-manager/src/SharedProjectsResource.ts index 893632b519..80c340dc49 100644 --- a/webapp/packages/core-resource-manager/src/SharedProjectsResource.ts +++ b/webapp/packages/core-resource-manager/src/SharedProjectsResource.ts @@ -67,16 +67,18 @@ export class SharedProjectsResource extends CachedMapResource { - await this.graphQLService.sdk.setProjectPermissions({ - projectId, + async addProjectPermissions(projectIds: string[], subjectIds: string[], permissions: string[]): Promise { + await this.graphQLService.sdk.addProjectsPermissions({ + projectIds, + subjectIds, permissions, }); } - async setSubjectProjectsAccess(subjectId: string, permissions: ProjectSubjectPermission[]): Promise { - await this.graphQLService.sdk.setSubjectProjectsPermissions({ - subjectId, + async deleteProjectPermissions(projectIds: string[], subjectIds: string[], permissions: string[]): Promise { + await this.graphQLService.sdk.deleteProjectsPermissions({ + projectIds, + subjectIds, permissions, }); } diff --git a/webapp/packages/core-sdk/src/queries/connections/administration/addConnectionsAccess.gql b/webapp/packages/core-sdk/src/queries/connections/administration/addConnectionsAccess.gql new file mode 100644 index 0000000000..a34e72c982 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/connections/administration/addConnectionsAccess.gql @@ -0,0 +1,3 @@ +query addConnectionsAccess($projectId: ID!, $connectionIds: [ID!]!, $subjects: [ID!]!) { + addConnectionsAccess(projectId: $projectId, connectionIds: $connectionIds, subjects: $subjects) +} diff --git a/webapp/packages/core-sdk/src/queries/connections/administration/deleteConnectionsAccess.gql b/webapp/packages/core-sdk/src/queries/connections/administration/deleteConnectionsAccess.gql new file mode 100644 index 0000000000..935c64468b --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/connections/administration/deleteConnectionsAccess.gql @@ -0,0 +1,3 @@ +query deleteConnectionsAccess($projectId: ID!, $connectionIds: [ID!]!, $subjects: [ID!]!) { + deleteConnectionsAccess(projectId: $projectId, connectionIds: $connectionIds, subjects: $subjects) +} diff --git a/webapp/packages/core-sdk/src/queries/connections/administration/setConnectionAccess.gql b/webapp/packages/core-sdk/src/queries/connections/administration/setConnectionAccess.gql deleted file mode 100644 index 6b536bd918..0000000000 --- a/webapp/packages/core-sdk/src/queries/connections/administration/setConnectionAccess.gql +++ /dev/null @@ -1,11 +0,0 @@ -query setConnectionAccess( - $projectId: ID! - $connectionId: ID! - $subjects: [ID!]! -) { - setConnectionSubjectAccess( - projectId: $projectId - connectionId: $connectionId - subjects: $subjects - ) -} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/connections/administration/setSubjectConnectionAccess.gql b/webapp/packages/core-sdk/src/queries/connections/administration/setSubjectConnectionAccess.gql deleted file mode 100644 index 9a64d6b677..0000000000 --- a/webapp/packages/core-sdk/src/queries/connections/administration/setSubjectConnectionAccess.gql +++ /dev/null @@ -1,3 +0,0 @@ -query setSubjectConnectionAccess($subjectId: ID!, $connections: [ID!]!) { - setSubjectConnectionAccess(subjectId: $subjectId, connections: $connections) -} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/resource-manager/addProjectsPermissions.gql b/webapp/packages/core-sdk/src/queries/resource-manager/addProjectsPermissions.gql new file mode 100644 index 0000000000..153c1d1cc9 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/resource-manager/addProjectsPermissions.gql @@ -0,0 +1,3 @@ +mutation addProjectsPermissions($projectIds: [ID!]!, $subjectIds: [ID!]!, $permissions: [String!]!) { + rmAddProjectsPermissions(projectIds: $projectIds, subjectIds: $subjectIds, permissions: $permissions) +} diff --git a/webapp/packages/core-sdk/src/queries/resource-manager/deleteProjectsPermissions.gql b/webapp/packages/core-sdk/src/queries/resource-manager/deleteProjectsPermissions.gql new file mode 100644 index 0000000000..a16a9eb9ef --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/resource-manager/deleteProjectsPermissions.gql @@ -0,0 +1,3 @@ +mutation deleteProjectsPermissions($projectIds: [ID!]!, $subjectIds: [ID!]!, $permissions: [String!]!) { + rmDeleteProjectsPermissions(projectIds: $projectIds, subjectIds: $subjectIds, permissions: $permissions) +} diff --git a/webapp/packages/core-sdk/src/queries/resource-manager/setProjectPermissions.gql b/webapp/packages/core-sdk/src/queries/resource-manager/setProjectPermissions.gql deleted file mode 100644 index fd7162fe9d..0000000000 --- a/webapp/packages/core-sdk/src/queries/resource-manager/setProjectPermissions.gql +++ /dev/null @@ -1,9 +0,0 @@ -mutation setProjectPermissions( - $projectId: String! - $permissions: [RMSubjectProjectPermissions!]! -) { - rmSetProjectPermissions( - projectId: $projectId - permissions: $permissions - ) -} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/resource-manager/setSubjectProjectsPermissions.gql b/webapp/packages/core-sdk/src/queries/resource-manager/setSubjectProjectsPermissions.gql deleted file mode 100644 index 72f0c463c7..0000000000 --- a/webapp/packages/core-sdk/src/queries/resource-manager/setSubjectProjectsPermissions.gql +++ /dev/null @@ -1,9 +0,0 @@ -mutation setSubjectProjectsPermissions( - $subjectId: String! - $permissions: [RMProjectPermissions!]! -) { - rmSetSubjectProjectPermissions( - subjectId: $subjectId - permissions: $permissions - ) -} \ No newline at end of file diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnectionsTabService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnectionsTabService.ts index 209a3ae815..eadaf25efd 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnectionsTabService.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnectionsTabService.ts @@ -13,7 +13,7 @@ import { NotificationService } from '@cloudbeaver/core-events'; import { executorHandlerFilter, IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { isGlobalProject, ProjectInfoResource } from '@cloudbeaver/core-projects'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import { GraphQLService } from '@cloudbeaver/core-sdk'; +import { AdminConnectionGrantInfo, GraphQLService } from '@cloudbeaver/core-sdk'; import { isArraysEqual, MetadataValueGetter } from '@cloudbeaver/core-utils'; import { teamContext } from '../Contexts/teamContext'; @@ -87,6 +87,12 @@ export class GrantedConnectionsTabService extends Bootstrap { return; } + const globalProject = this.projectInfoResource.values.find(isGlobalProject); + + if (!globalProject) { + throw new Error('The global project does not exist'); + } + const grantInfo = await this.teamsResource.getSubjectConnectionAccess(config.teamId); const initial = grantInfo.map(info => info.connectionId); @@ -96,15 +102,41 @@ export class GrantedConnectionsTabService extends Bootstrap { return; } + const { connectionsToRevoke, connectionsToGrant } = this.getConnectionsDifferences(grantInfo, state); + try { - await this.graphQLService.sdk.setSubjectConnectionAccess({ - subjectId: config.teamId, - connections: state.grantedSubjects, - }); + if (connectionsToRevoke.length > 0) { + await this.graphQLService.sdk.deleteConnectionsAccess({ + projectId: globalProject.id, + subjects: [config.teamId], + connectionIds: connectionsToRevoke, + }); + } + + if (connectionsToGrant.length > 0) { + await this.graphQLService.sdk.addConnectionsAccess({ + projectId: globalProject.id, + subjects: [config.teamId], + connectionIds: connectionsToGrant, + }); + } state.loaded = false; } catch (exception: any) { this.notificationService.logException(exception); } } + + private getConnectionsDifferences( + grantInfo: AdminConnectionGrantInfo[], + state: IGrantedConnectionsTabState, + ): { connectionsToRevoke: string[]; connectionsToGrant: string[] } { + const current = grantInfo.map(info => info.connectionId); + const next = state.grantedSubjects; + + const connectionsToRevoke = current.filter(connectionId => !next.includes(connectionId)); + const connectionsToGrant = next.filter(connectionId => !current.includes(connectionId)); + + return { connectionsToRevoke, connectionsToGrant }; + } } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/DATA_CONTEXT_USER_FORM_CONNECTION_ACCESS_PART.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/DATA_CONTEXT_USER_FORM_CONNECTION_ACCESS_PART.ts index 450dc041dc..6a1bd69f85 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/DATA_CONTEXT_USER_FORM_CONNECTION_ACCESS_PART.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/DATA_CONTEXT_USER_FORM_CONNECTION_ACCESS_PART.ts @@ -12,6 +12,7 @@ import { DATA_CONTEXT_FORM_STATE } from '@cloudbeaver/core-ui'; import type { AdministrationUserFormState } from '../AdministrationUserFormState'; import { DATA_CONTEXT_USER_FORM_INFO_PART } from '../Info/DATA_CONTEXT_USER_FORM_INFO_PART'; import { UserFormConnectionAccessPart } from './UserFormConnectionAccessPart'; +import { ProjectInfoResource } from '@cloudbeaver/core-projects'; export const DATA_CONTEXT_USER_FORM_CONNECTION_ACCESS_PART = createDataContext( 'User Form Connection Access Part', @@ -21,7 +22,8 @@ export const DATA_CONTEXT_USER_FORM_CONNECTION_ACCESS_PART = createDataContext { - constructor(formState: AdministrationUserFormState, private readonly usersResource: UsersResource) { + constructor( + formState: AdministrationUserFormState, + private readonly usersResource: UsersResource, + private readonly projectInfoResource: ProjectInfoResource, + ) { super(formState, []); } @@ -55,14 +60,39 @@ export class UserFormConnectionAccessPart extends FormPart 0) { + await this.usersResource.deleteConnectionsAccess(globalProject.id, userFormInfoPart.state.userId, connectionsToRevoke); + } + + if (connectionsToGrant.length > 0) { + await this.usersResource.addConnectionsAccess(globalProject.id, userFormInfoPart.state.userId, connectionsToGrant); + } } private getGrantedConnections(state: AdminConnectionGrantInfo[]): string[] { return state.filter(connection => connection.subjectType !== AdminSubjectType.Team).map(connection => connection.dataSourceId); } + private getConnectionsDifferences(current: string[], next: string[]): { connectionsToRevoke: string[]; connectionsToGrant: string[] } { + const connectionsToRevoke = current.filter(subjectId => !next.includes(subjectId)); + const connectionsToGrant = next.filter(subjectId => !current.includes(subjectId)); + + return { connectionsToRevoke, connectionsToGrant }; + } + protected override async loader() { const userFormInfoPart = this.formState.dataContext.get(DATA_CONTEXT_USER_FORM_INFO_PART); let grantedConnections: AdminConnectionGrantInfo[] = []; diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessTabService.ts b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessTabService.ts index bb1371c0a1..1771c53a58 100644 --- a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessTabService.ts +++ b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessTabService.ts @@ -104,12 +104,24 @@ export class ConnectionAccessTabService extends Bootstrap { const key = createConnectionParam(data.state.projectId, config.connectionId); - const changed = await this.isChanged(key, state.grantedSubjects); + const currentGrantedSubjects = await this.connectionInfoResource.loadAccessSubjects(key); + const currentGrantedSubjectIds = currentGrantedSubjects.map(subject => subject.subjectId); - if (changed) { - await this.connectionInfoResource.setAccessSubjects(key, state.grantedSubjects); - state.initialGrantedSubjects = state.grantedSubjects.slice(); + const { subjectsToRevoke, subjectsToGrant } = this.getSubjectDifferences(currentGrantedSubjectIds, state.grantedSubjects); + + if (subjectsToRevoke.length === 0 && subjectsToGrant.length === 0) { + return; + } + + if (subjectsToRevoke.length > 0) { + await this.connectionInfoResource.deleteConnectionsAccess(key, subjectsToRevoke); } + + if (subjectsToGrant.length > 0) { + await this.connectionInfoResource.addConnectionsAccess(key, subjectsToGrant); + } + + state.initialGrantedSubjects = state.grantedSubjects.slice(); } private async formState(data: IConnectionFormState, contexts: IExecutionContextProvider) { @@ -138,4 +150,11 @@ export class ConnectionAccessTabService extends Bootstrap { return current.some(value => !next.some(subjectId => subjectId === value.subjectId)); } + + private getSubjectDifferences(current: string[], next: string[]): { subjectsToRevoke: string[]; subjectsToGrant: string[] } { + const subjectsToRevoke = current.filter(subjectId => !next.includes(subjectId)); + const subjectsToGrant = next.filter(subjectId => !current.includes(subjectId)); + + return { subjectsToRevoke, subjectsToGrant }; + } }