diff --git a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF index ce25b3dc57..7943873688 100644 --- a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF @@ -29,6 +29,8 @@ Export-Package: io.cloudbeaver, io.cloudbeaver.registry, io.cloudbeaver.server, io.cloudbeaver.service, + io.cloudbeaver.service.admin, + io.cloudbeaver.service.security, io.cloudbeaver.service.sql, io.cloudbeaver.utils Automatic-Module-Name: io.cloudbeaver.model diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java index 82e5ca35c2..40e714f34e 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java @@ -346,7 +346,7 @@ public class WebConnectionInfo { @Property public boolean isCanEdit() { - return hasProjectPermission(RMProjectPermission.CONNECTIONS_EDIT); + return hasProjectPermission(RMProjectPermission.DATA_SOURCES_EDIT); } @Property diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebProjectInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebProjectInfo.java index 73baf1cf1a..1c594a6cde 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebProjectInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebProjectInfo.java @@ -18,6 +18,7 @@ package io.cloudbeaver.model; import io.cloudbeaver.VirtualProjectImpl; import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.service.security.SMUtils; import org.jkiss.dbeaver.model.app.DBPProject; import org.jkiss.dbeaver.model.meta.Property; import org.jkiss.dbeaver.model.rm.RMProjectPermission; @@ -55,16 +56,27 @@ public class WebProjectInfo { } @Property - public boolean isCanCreateConnections() { - return hasRmPermission(RMProjectPermission.CONNECTIONS_EDIT); + public boolean isCanEditDataSources() { + return hasDataSourcePermission(RMProjectPermission.DATA_SOURCES_EDIT); } @Property - public boolean isCanViewConnections() { - return hasRmPermission(RMProjectPermission.CONNECTIONS_VIEW); + public boolean isCanViewDataSources() { + return hasDataSourcePermission(RMProjectPermission.DATA_SOURCES_VIEW); } - private boolean hasRmPermission(RMProjectPermission permission) { - return project.getRmProject().getProjectPermissions().contains(permission.getPermissionId()); + @Property + public boolean isCanEditResources() { + return hasDataSourcePermission(RMProjectPermission.RESOURCE_EDIT); + } + + @Property + public boolean isCanViewResources() { + return hasDataSourcePermission(RMProjectPermission.RESOURCE_VIEW); + } + + private boolean hasDataSourcePermission(RMProjectPermission permission) { + return SMUtils.isRMAdmin(session) + || project.getRmProject().getProjectPermissions().contains(permission.getPermissionId()); } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/RMUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/RMUtils.java index 8e88f4eeca..4cc972a770 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/RMUtils.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/RMUtils.java @@ -49,7 +49,7 @@ public class RMUtils { RMProject project = new RMProject("anonymous"); project.setId("anonymous"); project.setType(RMProject.Type.USER); - project.setProjectPermissions(RMProjectPermission.CONNECTIONS_EDIT.getAllPermissions()); + project.setProjectPermissions(RMProjectPermission.DATA_SOURCES_EDIT.getAllPermissions()); return project; } } 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 3e01ff4b99..7270a19da3 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 @@ -19,6 +19,7 @@ package io.cloudbeaver.model.rm.local; import io.cloudbeaver.DBWConstants; import io.cloudbeaver.VirtualProjectImpl; import io.cloudbeaver.model.rm.RMUtils; +import io.cloudbeaver.service.security.SMUtils; import io.cloudbeaver.service.sql.WebSQLConstants; import io.cloudbeaver.utils.WebAppUtils; import org.eclipse.core.runtime.IPath; @@ -160,7 +161,7 @@ public class LocalResourceController implements RMController { return accessibleSharedProjects .stream() .map(projectPermission -> makeProjectFromPath( - sharedProjectsPath.resolve(projectPermission.getObjectId()), + sharedProjectsPath.resolve(parseProjectName(projectPermission.getObjectId()).getName()), projectPermission.getPermissions().stream().map(RMProjectPermission::fromPermission).collect(Collectors.toSet()), RMProject.Type.SHARED, true) ) @@ -173,20 +174,25 @@ public class LocalResourceController implements RMController { switch (projectType) { case GLOBAL: - return credentialsProvider.hasPermission(DBWConstants.PERMISSION_ADMIN) - ? Set.of(RMProjectPermission.RESOURCE_EDIT, RMProjectPermission.CONNECTIONS_EDIT) - : Set.of(RMProjectPermission.RESOURCE_VIEW, RMProjectPermission.CONNECTIONS_VIEW); + return SMUtils.isRMAdmin(credentialsProvider) + ? Set.of(RMProjectPermission.PROJECT_ADMIN) + : Set.of(RMProjectPermission.RESOURCE_VIEW, RMProjectPermission.DATA_SOURCES_VIEW); case SHARED: - if(projectId == null) { + if (SMUtils.isRMAdmin(credentialsProvider)) { + return Set.of(RMProjectPermission.PROJECT_ADMIN); + } + + if (projectId == null) { throw new DBException("Project id required"); } + return smController.getObjectPermissions(activeUserCreds.getUserId(), projectId, SMObjects.PROJECT) .getPermissions() .stream() .map(RMProjectPermission::fromPermission) .collect(Collectors.toSet()); case USER: - return Set.of(RMProjectPermission.RESOURCE_EDIT, RMProjectPermission.CONNECTIONS_EDIT); + return Set.of(RMProjectPermission.RESOURCE_EDIT, RMProjectPermission.DATA_SOURCES_EDIT); default: throw new DBException("Unknown project type:" + projectType); } @@ -199,8 +205,17 @@ public class LocalResourceController implements RMController { if (!Files.exists(sharedProjectsPath)) { return new RMProject[0]; } - return Files.list(sharedProjectsPath) - .map((Path path) -> makeProjectFromPath(path, Set.of(), RMProject.Type.SHARED, false)) + var projects = new ArrayList(); + var allPaths = Files.list(sharedProjectsPath).collect(Collectors.toList()); + for (Path path : allPaths) { + var projectPerms = getProjectPermissions( + makeProjectIdFromPath(path, RMProject.Type.SHARED), + RMProject.Type.SHARED + ); + var rmProject = makeProjectFromPath(path, projectPerms, RMProject.Type.SHARED, false); + projects.add(rmProject); + } + return projects.stream() .filter(Objects::nonNull) .toArray(RMProject[]::new); } catch (IOException e) { @@ -244,6 +259,7 @@ public class LocalResourceController implements RMController { } try { CommonUtils.deleteDirectory(targetPath); + smController.deleteAllObjectPermissions(projectId, SMObjects.PROJECT); } catch (IOException e) { throw new DBException("Error deleting project '" + project.getName() + "'", e); } @@ -525,11 +541,16 @@ public class LocalResourceController implements RMController { } + private String makeProjectIdFromPath(Path path, RMProject.Type type) { + String projectName = path.getFileName().toString(); + return type.getPrefix() + "_" + projectName; + } + private RMProject makeProjectFromId(String projectId, boolean loadPermissions) throws DBException { var projectName = parseProjectName(projectId); var projectPath = getProjectPath(projectId); Set permissions = Set.of(); - if(loadPermissions && credentialsProvider.getActiveUserCredentials() != null) { + if (loadPermissions && credentialsProvider.getActiveUserCredentials() != null) { permissions = getProjectPermissions(projectId, projectName.getType()); } return makeProjectFromPath(projectPath, permissions, projectName.getType(), false); @@ -555,7 +576,7 @@ public class LocalResourceController implements RMController { RMProject project = new RMProject(); String projectName = path.getFileName().toString(); project.setName(projectName); - project.setId(type.getPrefix() + "_" + projectName); + project.setId(makeProjectIdFromPath(path, type)); project.setType(type); project.setProjectPermissions(allProjectPermissions); if (Files.exists(path)) { @@ -750,10 +771,9 @@ public class LocalResourceController implements RMController { return new RMProjectName(prefix, name); } - public static boolean isShared(String projectId) { + public static boolean isGlobalProject(String projectId) { RMProjectName rmProjectName = parseProjectName(projectId); - return RMProject.Type.SHARED.getPrefix().equals(rmProjectName.getPrefix()) || - RMProject.Type.GLOBAL.getPrefix().equals(rmProjectName.getPrefix()); + return RMProject.Type.GLOBAL.getPrefix().equals(rmProjectName.getPrefix()); } } 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 ff34f9cf17..3094f6a702 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 @@ -332,7 +332,7 @@ public class WebSession extends AbstractSessionPersistent implements SMSession, RMProject[] rmProjects = controller.listAccessibleProjects(); for (RMProject project : rmProjects) { VirtualProjectImpl virtualProject = createVirtualProject(project); - if (!virtualProject.getRmProject().getProjectPermissions().contains(RMProjectPermission.CONNECTIONS_EDIT.getPermissionId())) { + if (!virtualProject.getRmProject().getProjectPermissions().contains(RMProjectPermission.DATA_SOURCES_EDIT.getPermissionId())) { // Projects for which user don't have edit permission can't be saved. So mark the as in memory virtualProject.setInMemory(true); } @@ -349,7 +349,9 @@ public class WebSession extends AbstractSessionPersistent implements SMSession, public VirtualProjectImpl createVirtualProject(RMProject project) { // Do not filter data sources from user project - DataSourceFilter filter = project.getType() == RMProject.Type.USER ? x -> true : this::isDataSourceAccessible; + DataSourceFilter filter = project.getType() == RMProject.Type.GLOBAL + ? this::isDataSourceAccessible + : x -> true; VirtualProjectImpl sessionProject = application.createProjectImpl( project, sessionAuthContext, diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminPermissionInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/admin/AdminPermissionInfo.java similarity index 100% rename from server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminPermissionInfo.java rename to server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/admin/AdminPermissionInfo.java diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/security/SMUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/security/SMUtils.java new file mode 100644 index 0000000000..fb925fa737 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/security/SMUtils.java @@ -0,0 +1,37 @@ +package io.cloudbeaver.service.security; + +import io.cloudbeaver.DBWConstants; +import io.cloudbeaver.registry.WebPermissionDescriptor; +import io.cloudbeaver.registry.WebServiceDescriptor; +import io.cloudbeaver.registry.WebServiceRegistry; +import io.cloudbeaver.service.admin.AdminPermissionInfo; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; +import org.jkiss.dbeaver.model.rm.RMConstants; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class SMUtils { + public static boolean isAdmin(SMCredentialsProvider webSession) { + return webSession.hasPermission(DBWConstants.PERMISSION_ADMIN); + } + + public static boolean isRMAdmin(SMCredentialsProvider webSession) { + return isAdmin(webSession) || webSession.hasPermission(RMConstants.PERMISSION_RM_ADMIN); + } + + public static List findPermissions(@NotNull String permissionsScope) { + List permissionInfos = new ArrayList<>(); + for (WebServiceDescriptor wsd : WebServiceRegistry.getInstance().getWebServices()) { + for (WebPermissionDescriptor pd : wsd.getPermissions()) { + if (permissionsScope.equals(pd.getScope())) { + permissionInfos.add(new AdminPermissionInfo(pd)); + } + } + } + permissionInfos.sort(Comparator.comparing(AdminPermissionInfo::getLabel)); + return permissionInfos; + } +} diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index c097dd45e4..0e8498648b 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -370,8 +370,10 @@ type ProjectInfo { id: String! name: String! description: String - canCreateConnections: Boolean! - canViewConnections: Boolean! + canEditDataSources: Boolean! + canViewDataSources: Boolean! + canEditResources: Boolean! + canViewResources: Boolean! } type LogEntry { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java index 99988a3d5c..d7b7248a2b 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java @@ -182,7 +182,9 @@ public abstract class WebServiceBindingBase impleme private void checkObjectActionPermissions(Method method, WebProjectAction objectAction, Object[] args) throws DBException { WebSession webSession = findWebSession(env); - + if (webSession.hasPermission(DBWConstants.PERMISSION_ADMIN)) { + return; + } String[] requireProjectPermissions = objectAction.requireProjectPermissions(); if (requireProjectPermissions.length > 0) { int objectIdArgumentIndex = -1; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java index 99bbb2eb79..c1c2d412c7 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java @@ -111,32 +111,32 @@ public interface DBWServiceCore extends DBWService { @Nullable List networkCredentials, @Nullable Boolean saveCredentials) throws DBWebException; - @WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_CONNECTIONS_EDIT}) + @WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT}) WebConnectionInfo createConnection( @NotNull WebSession webSession, @Nullable @WebObjectId String projectId, @NotNull WebConnectionConfig connectionConfig) throws DBWebException; - @WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_CONNECTIONS_EDIT}) + @WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT}) WebConnectionInfo updateConnection( @NotNull WebSession webSession, @Nullable @WebObjectId String projectId, @NotNull WebConnectionConfig connectionConfig) throws DBWebException; - @WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_CONNECTIONS_EDIT}) + @WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT}) boolean deleteConnection( @NotNull WebSession webSession, @Nullable @WebObjectId String projectId, @NotNull String connectionId) throws DBWebException; - @WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_CONNECTIONS_EDIT}) + @WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT}) WebConnectionInfo createConnectionFromTemplate( @NotNull WebSession webSession, @Nullable @WebObjectId String projectId, @NotNull String templateId, @Nullable String connectionName) throws DBWebException; - @WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_CONNECTIONS_EDIT}) + @WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT}) WebConnectionInfo copyConnectionFromNode( @NotNull WebSession webSession, @Nullable @WebObjectId String projectId, @@ -162,14 +162,14 @@ public interface DBWServiceCore extends DBWService { /////////////////////////////////////////// // Folders - @WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_CONNECTIONS_EDIT}) + @WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT}) WebConnectionFolderInfo createConnectionFolder( @NotNull WebSession session, @Nullable @WebObjectId String projectId, @NotNull String parentNodePath, @NotNull String newName) throws DBWebException; - @WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_CONNECTIONS_EDIT}) + @WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT}) WebConnectionFolderInfo renameConnectionFolder( @NotNull WebSession session, @Nullable @WebObjectId @@ -177,7 +177,7 @@ public interface DBWServiceCore extends DBWService { @NotNull String folderPath, @NotNull String newName) throws DBWebException; - @WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_CONNECTIONS_EDIT}) + @WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT}) boolean deleteConnectionFolder( @NotNull WebSession session, @Nullable @WebObjectId String projectId, diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java index bd490aade0..428b341c8c 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java @@ -34,7 +34,6 @@ import org.jkiss.dbeaver.model.struct.DBSEntity; import org.jkiss.dbeaver.model.struct.DBSObject; import org.jkiss.dbeaver.model.struct.rdb.DBSProcedure; import org.jkiss.dbeaver.registry.DataSourceFolder; -import org.jkiss.dbeaver.registry.DataSourceRegistry; import org.jkiss.dbeaver.runtime.DBWorkbench; import java.util.ArrayList; @@ -185,7 +184,7 @@ public class WebNavigatorNodeInfo { return features.toArray(new String[0]); } VirtualProjectImpl project = session.getProjectById(node.getOwnerProject().getId()); - if (!project.getRmProject().getProjectPermissions().contains(RMProjectPermission.CONNECTIONS_EDIT.getPermissionId())) { + if (!project.getRmProject().getProjectPermissions().contains(RMProjectPermission.DATA_SOURCES_EDIT.getPermissionId())) { return features.toArray(new String[0]); } if (node instanceof DBNLocalFolder || node instanceof DBNResourceManagerResource) { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java index 5b01f9b60d..fcea315a62 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java @@ -432,8 +432,7 @@ public class WebServiceNavigator implements DBWServiceNavigator { private void checkProjectEditAccess(DBNNode node, WebSession session) throws DBException { var project = session.getProjectById(node.getOwnerProject().getId()); - if (project == null - || !hasNodeEditPermission(node, project.getRmProject()) + if (project == null || !hasNodeEditPermission(node, project.getRmProject()) ) { throw new DBException("Access denied"); } @@ -442,7 +441,7 @@ public class WebServiceNavigator implements DBWServiceNavigator { private boolean hasNodeEditPermission(DBNNode node, RMProject rmProject) { var projectPermissions = rmProject.getProjectPermissions(); if (node instanceof DBNDataSource || node instanceof DBNLocalFolder) { - return projectPermissions.contains(RMProjectPermission.CONNECTIONS_EDIT.getPermissionId()); + return projectPermissions.contains(RMProjectPermission.DATA_SOURCES_EDIT.getPermissionId()); } else if (node instanceof DBNAbstractResourceManagerNode) { return projectPermissions.contains(RMProjectPermission.RESOURCE_EDIT.getPermissionId()); } 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 dd27bbb77c..d7f3029352 100644 --- a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls +++ b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls @@ -11,6 +11,17 @@ type AdminConnectionGrantInfo { subjectType: AdminSubjectType! } +type AdminObjectPermissions { + objectId: ID! + permissions: [String!]! +} + +type AdminObjectGrantInfo { + subjectId: ID! + subjectType: AdminSubjectType! + objectPermissions: AdminObjectPermissions! +} + type AdminConnectionSearchInfo { displayName: String! host: String! 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 9671492103..002b0fa820 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 @@ -37,6 +37,7 @@ import io.cloudbeaver.server.CBConstants; import io.cloudbeaver.server.CBPlatform; import io.cloudbeaver.service.DBWServiceServerConfigurator; import io.cloudbeaver.service.admin.*; +import io.cloudbeaver.service.security.SMUtils; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; @@ -118,18 +119,9 @@ public class WebServiceAdmin implements DBWServiceAdmin { @Override public List listPermissions(@NotNull WebSession webSession) throws DBWebException { try { - List permissionInfos = new ArrayList<>(); - for (WebServiceDescriptor wsd : WebServiceRegistry.getInstance().getWebServices()) { - for (WebPermissionDescriptor pd : wsd.getPermissions()) { - if(SMConstants.SUBJECT_PERMISSION_SCOPE.equals(pd.getScope())) { - permissionInfos.add(new AdminPermissionInfo(pd)); - } - } - } - permissionInfos.sort(Comparator.comparing(AdminPermissionInfo::getLabel)); - return permissionInfos; + return SMUtils.findPermissions(SMConstants.SUBJECT_PERMISSION_SCOPE); } catch (Exception e) { - throw new DBWebException("Error reading users", e); + throw new DBWebException("Error reading permissions", e); } } diff --git a/server/bundles/io.cloudbeaver.service.rm/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.rm/META-INF/MANIFEST.MF index 896f69eb89..35e892b5d4 100644 --- a/server/bundles/io.cloudbeaver.service.rm/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.rm/META-INF/MANIFEST.MF @@ -8,6 +8,7 @@ Bundle-Release-Date: 20220926 Bundle-RequiredExecutionEnvironment: JavaSE-11 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . -Require-Bundle: io.cloudbeaver.server +Require-Bundle: io.cloudbeaver.server, + io.cloudbeaver.service.admin Bundle-Localization: OSGI-INF/l10n/bundle Automatic-Module-Name: io.cloudbeaver.service.rm diff --git a/server/bundles/io.cloudbeaver.service.rm/plugin.xml b/server/bundles/io.cloudbeaver.service.rm/plugin.xml index ca3ff4442b..5d8f5865c6 100644 --- a/server/bundles/io.cloudbeaver.service.rm/plugin.xml +++ b/server/bundles/io.cloudbeaver.service.rm/plugin.xml @@ -5,15 +5,16 @@ - - + + - - + + 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 3d3e6f8050..fcffb05fa5 100644 --- a/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls +++ b/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls @@ -17,6 +17,11 @@ type RMResource { length: Int! } +input RMProjectPermissions { + subjectId: String! + permissions: [String!]! +} + extend type Query { # List accessible projects @@ -26,7 +31,12 @@ extend type Query { rmProject(projectId: String!): RMProject! - # List accessible projects + rmListProjectPermissions: [AdminPermissionInfo!]! + + rmListProjectGrantedPermissions(projectId: String!): [AdminObjectGrantInfo!]! + + rmListSubjectProjectsPermissionGrants(subjectId: String!): [AdminObjectGrantInfo!]! + rmListResources( projectId: String!, folder: String, @@ -70,4 +80,7 @@ extend type Mutation { description: String): RMProject! rmDeleteProject(projectId: ID!): Boolean! + + rmSetProjectPermissions(projectId: String!, permissions: [RMProjectPermissions!]!): 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 15471ca396..863b79fa6e 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 @@ -17,15 +17,19 @@ package io.cloudbeaver.service.rm; import io.cloudbeaver.*; -import io.cloudbeaver.model.WebProjectInfo; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.service.DBWService; +import io.cloudbeaver.service.admin.AdminPermissionInfo; +import io.cloudbeaver.service.rm.model.RMSubjectProjectPermissions; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.rm.RMConstants; import org.jkiss.dbeaver.model.rm.RMProject; import org.jkiss.dbeaver.model.rm.RMResource; +import org.jkiss.dbeaver.model.security.SMObjectPermissionsGrant; + +import java.util.List; /** * Web service API @@ -99,4 +103,30 @@ public interface DBWServiceRM extends DBWService { boolean deleteProject( @NotNull WebSession session, @NotNull @WebObjectId String projectId) throws DBWebException; + + @WebAction(requirePermissions = {RMConstants.PERMISSION_RM_ADMIN}) + List listProjectPermissions() throws DBWebException; + + @WebProjectAction( + requireProjectPermissions = RMConstants.PERMISSION_PROJECT_ADMIN + ) + boolean setProjectPermissions( + @NotNull WebSession webSession, + @NotNull @WebObjectId String projectId, + @NotNull RMSubjectProjectPermissions projectPermissions + ) throws DBWebException; + + @WebProjectAction( + requireProjectPermissions = RMConstants.PERMISSION_PROJECT_ADMIN + ) + List listProjectGrantedPermissions( + @NotNull WebSession webSession, + @NotNull @WebObjectId String projectId + ) throws DBWebException; + + @WebAction(requirePermissions = {RMConstants.PERMISSION_RM_ADMIN}) + List listSubjectProjectsPermissionGrants( + @NotNull WebSession webSession, + @NotNull String subjectId + ) throws DBWebException; } 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 687a1a8e4a..5c956ddbd2 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 @@ -20,6 +20,7 @@ import io.cloudbeaver.DBWebException; import io.cloudbeaver.service.DBWBindingContext; import io.cloudbeaver.service.WebServiceBindingBase; import io.cloudbeaver.service.rm.impl.WebServiceRM; +import io.cloudbeaver.service.rm.model.RMSubjectProjectPermissions; import org.jkiss.utils.CommonUtils; /** @@ -53,6 +54,15 @@ public class WebServiceBindingRM extends WebServiceBindingBase { env -> getService(env).readResourceAsString(getWebSession(env), env.getArgument("projectId"), env.getArgument("resourcePath"))) + .dataFetcher("rmListProjectPermissions", env -> getService(env).listProjectPermissions()) + .dataFetcher("rmListProjectGrantedPermissions", env -> getService(env).listProjectGrantedPermissions( + getWebSession(env), + env.getArgument("projectId") + )) + .dataFetcher("rmListSubjectProjectsPermissionGrants", env -> getService(env).listSubjectProjectsPermissionGrants( + getWebSession(env), + env.getArgument("subjectId") + )) ; model.getMutationType() .dataFetcher("rmCreateResource", @@ -80,6 +90,11 @@ public class WebServiceBindingRM extends WebServiceBindingBase { getWebSession(env), getProjectReference(env) )) + .dataFetcher("rmSetProjectPermissions", env -> getService(env).setProjectPermissions( + getWebSession(env), + env.getArgument("projectId"), + new RMSubjectProjectPermissions(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 ed3403c1f0..cdbb56f95f 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 @@ -17,9 +17,11 @@ package io.cloudbeaver.service.rm.impl; import io.cloudbeaver.DBWebException; -import io.cloudbeaver.model.WebProjectInfo; import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.service.admin.AdminPermissionInfo; import io.cloudbeaver.service.rm.DBWServiceRM; +import io.cloudbeaver.service.rm.model.RMSubjectProjectPermissions; +import io.cloudbeaver.service.security.SMUtils; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; @@ -27,8 +29,12 @@ 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 java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.Set; /** * Web service implementation @@ -148,6 +154,61 @@ public class WebServiceRM implements DBWServiceRM { } } + @Override + public List listProjectPermissions() throws DBWebException { + try { + return SMUtils.findPermissions(SMConstants.PROJECT_PERMISSION_SCOPE); + } catch (Exception e) { + throw new DBWebException("Error reading project permissions", e); + } + } + + @Override + public boolean setProjectPermissions(@NotNull WebSession webSession, + @NotNull String projectId, + @NotNull RMSubjectProjectPermissions projectPermissions + ) throws DBWebException { + try { + SMController sm = webSession.getSecurityController(); + //TODO one 'setObjectPermissions' call + sm.deleteAllObjectPermissions(projectId, SMObjects.PROJECT); + for (Map.Entry> entry : projectPermissions.getSubjectPermissions().entrySet()) { + String subjectId = entry.getKey(); + Set permissions = entry.getValue(); + sm.setObjectPermissions( + Set.of(projectId), + SMObjects.PROJECT, + Set.of(subjectId), + permissions, + webSession.getUserId() + ); + } + return true; + } catch (Exception e) { + throw new DBWebException("Error granting project permissions", e); + } + } + + @Override + public List listProjectGrantedPermissions(@NotNull WebSession webSession, @NotNull String projectId) throws DBWebException { + SMController sm = webSession.getSecurityController(); + try { + return sm.getObjectPermissionGrants(projectId, SMObjects.PROJECT); + } catch (DBException e) { + throw new DBWebException("Error reading project permission grants", e); + } + } + + @Override + public List listSubjectProjectsPermissionGrants(@NotNull WebSession webSession, @NotNull String subjectId) throws DBWebException { + try { + SMAdminController sm = webSession.getAdminSecurityController(); + return sm.getSubjectObjectPermissionGrants(subjectId, SMObjects.PROJECT); + } catch (DBException e) { + throw new DBWebException("Error reading project permission grants", e); + } + } + private RMController getResourceController(WebSession webSession) { return webSession.getRmController(); } diff --git a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/model/RMSubjectProjectPermissions.java b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/model/RMSubjectProjectPermissions.java new file mode 100644 index 0000000000..fcd355fbb6 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/model/RMSubjectProjectPermissions.java @@ -0,0 +1,21 @@ +package io.cloudbeaver.service.rm.model; + +import org.jkiss.dbeaver.model.data.json.JSONUtils; + +import java.util.*; + +public class RMSubjectProjectPermissions { + private final Map> subjectPermissions = new HashMap<>(); + + public RMSubjectProjectPermissions(List> params) { + for (Map userPerms : params) { + String subjectId = JSONUtils.getString(userPerms, "subjectId"); + Set permissions = new HashSet<>(JSONUtils.getStringList(userPerms, "permissions")); + this.subjectPermissions.put(subjectId, permissions); + } + } + + public Map> getSubjectPermissions() { + return subjectPermissions; + } +} diff --git a/webapp/packages/core-administration/src/PermissionsResource.ts b/webapp/packages/core-administration/src/PermissionsResource.ts index ff81ae8ec4..1f7c496020 100644 --- a/webapp/packages/core-administration/src/PermissionsResource.ts +++ b/webapp/packages/core-administration/src/PermissionsResource.ts @@ -8,9 +8,10 @@ import { injectable } from '@cloudbeaver/core-di'; import { SessionDataResource } from '@cloudbeaver/core-root'; -import { GraphQLService, AdminPermissionInfo, CachedMapResource, CachedMapAllKey } from '@cloudbeaver/core-sdk'; +import { GraphQLService, CachedMapResource, CachedMapAllKey, AdminPermissionInfoFragment, AdminObjectGrantInfoFragment } from '@cloudbeaver/core-sdk'; -export type PermissionInfo = AdminPermissionInfo; +export type PermissionInfo = AdminPermissionInfoFragment; +export type AdminObjectGrantInfo = AdminObjectGrantInfoFragment; @injectable() export class PermissionsResource extends CachedMapResource { diff --git a/webapp/packages/core-app/src/AppScreen/AppScreen.tsx b/webapp/packages/core-app/src/AppScreen/AppScreen.tsx index ecd42da6b1..cfd4920579 100644 --- a/webapp/packages/core-app/src/AppScreen/AppScreen.tsx +++ b/webapp/packages/core-app/src/AppScreen/AppScreen.tsx @@ -6,6 +6,8 @@ * you may not use this file except in compliance with the License. */ +import { memo } from 'react'; + import { Placeholder } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { TopNavBar } from '@cloudbeaver/plugin-top-app-bar'; @@ -13,7 +15,7 @@ import { TopNavBar } from '@cloudbeaver/plugin-top-app-bar'; import { AppScreenService } from './AppScreenService'; import { Main } from './Main'; -export function AppScreen() { +export const AppScreen = memo(function AppScreen() { const appScreenService = useService(AppScreenService); return ( <> @@ -22,4 +24,4 @@ export function AppScreen() {
); -} +}); diff --git a/webapp/packages/core-app/src/AppScreen/Main.tsx b/webapp/packages/core-app/src/AppScreen/Main.tsx index a9602ae7c2..f5f0eff0de 100644 --- a/webapp/packages/core-app/src/AppScreen/Main.tsx +++ b/webapp/packages/core-app/src/AppScreen/Main.tsx @@ -9,12 +9,8 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; -import { - splitStyles, Split, ResizerControls, Pane, ErrorBoundary, useMapResource, useSplitUserState -} from '@cloudbeaver/core-blocks'; -import { ConnectionExecutionContextResource, ConnectionInfoResource } from '@cloudbeaver/core-connections'; +import { splitStyles, Split, ResizerControls, Pane, ErrorBoundary, useSplitUserState } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { CachedMapAllKey } from '@cloudbeaver/core-sdk'; import { useStyles } from '@cloudbeaver/core-theming'; import { SideBarPanel, SideBarPanelService } from '@cloudbeaver/core-ui'; import { NavigationTree } from '@cloudbeaver/plugin-navigation-tree'; @@ -42,12 +38,6 @@ export const Main = observer(function Main() { const styles = useStyles(mainStyles, splitStyles); const splitState = useSplitUserState('main'); - // useMapResource( - // Main, - // ConnectionExecutionContextResource, - // CachedMapAllKey - // ); - // useMapResource(Main, ConnectionInfoResource, CachedMapAllKey); const activeBars = sideBarPanelService.tabsContainer.getDisplayed(); diff --git a/webapp/packages/core-blocks/src/Button.tsx b/webapp/packages/core-blocks/src/Button.tsx index 75ee9ca8a8..73d6a5c7a9 100644 --- a/webapp/packages/core-blocks/src/Button.tsx +++ b/webapp/packages/core-blocks/src/Button.tsx @@ -109,6 +109,7 @@ export const Button = observer(function Button({ mod, styles, tag = 'button', + type = 'button', disabled = false, loading, loader, @@ -143,7 +144,14 @@ export const Button = observer(function Button({ const Button = tag; return styled(useStyles(styles, buttonStyles, ...(mod || []).map(mod => buttonMod[mod])))( - + + + + )} + fixedWidth + onReject={rejectDialog} + > + + + {selectProject && ( + state.setProjectId(projectId)} + /> + )} + state.validate()} + > + {translate('ui_name') + ':'} + + + + + ); +}); diff --git a/webapp/packages/plugin-projects/src/LocaleService.ts b/webapp/packages/plugin-projects/src/LocaleService.ts new file mode 100644 index 0000000000..c6a5d9b3c9 --- /dev/null +++ b/webapp/packages/plugin-projects/src/LocaleService.ts @@ -0,0 +1,36 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 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 { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { LocalizationService } from '@cloudbeaver/core-localization'; + +@injectable() +export class LocaleService extends Bootstrap { + constructor(private readonly localizationService: LocalizationService) { + super(); + } + + register(): void | Promise { + this.localizationService.addProvider(this.provider.bind(this)); + } + + load(): void | Promise { } + + private async provider(locale: string) { + switch (locale) { + case 'ru': + return (await import('./locales/ru')).default; + case 'it': + return (await import('./locales/it')).default; + case 'zh': + return (await import('./locales/zh')).default; + default: + return (await import('./locales/en')).default; + } + } +} diff --git a/webapp/packages/plugin-projects/src/PluginBootstrap.ts b/webapp/packages/plugin-projects/src/PluginBootstrap.ts new file mode 100644 index 0000000000..b150d7c0a9 --- /dev/null +++ b/webapp/packages/plugin-projects/src/PluginBootstrap.ts @@ -0,0 +1,22 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 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 { Bootstrap, injectable } from '@cloudbeaver/core-di'; + +@injectable() +export class PluginBootstrap extends Bootstrap { + constructor( + ) { + super(); + } + + async load(): Promise { } + + register(): void { + } +} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProjectSelect.tsx b/webapp/packages/plugin-projects/src/ProjectSelect.tsx similarity index 67% rename from webapp/packages/plugin-connections/src/ConnectionForm/Options/ProjectSelect.tsx rename to webapp/packages/plugin-projects/src/ProjectSelect.tsx index 804bb3e9d8..cf0b4520db 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProjectSelect.tsx +++ b/webapp/packages/plugin-projects/src/ProjectSelect.tsx @@ -15,37 +15,50 @@ import { CachedMapAllKey } from '@cloudbeaver/core-sdk'; interface Props { value: string | null; + filter?: (project: ProjectInfo) => boolean; onChange: (value: string) => void; + autoHide?: boolean; readOnly?: boolean; disabled?: boolean; inline?: boolean; } -export const ProjectSelect = observer(function ProjectSelect(props: Props) { +export const ProjectSelect = observer(function ProjectSelect({ + value, + filter = project => project.canEditDataSources, + autoHide, + readOnly, + disabled, + inline, + onChange, +}: Props) { const translate = useTranslate(); const projectsService = useService(ProjectsService); const projectsLoader = useMapResource(ProjectSelect, ProjectInfoResource, CachedMapAllKey, { onData: () => { - if (!props.value && projectsService.activeProject) { - props.onChange(projectsService.activeProject.id); + if (!value && projectsService.activeProject) { + onChange(projectsService.activeProject.id); } }, }); - const value = props.value ?? projectsService.activeProject?.id; const projects = projectsLoader.data as ProjectInfo[]; - const possibleOptions = projects.filter(project => project.canCreateConnections); + const possibleOptions = projects.filter(filter); function handleProjectSelect(projectId: string) { const project = projectsLoader.resource.get(projectId); - if (project?.canCreateConnections) { - props.onChange(projectId); + if (project && filter(project)) { + onChange(projectId); } } + if (autoHide && possibleOptions.length <= 1) { + return null; + } + return ( project.id} valueSelector={project => project.name} titleSelector={project => project.description} - isDisabled={project => !project.canCreateConnections} - readOnly={props.readOnly || possibleOptions.length <= 1} + isDisabled={project => !project.canEditDataSources} + readOnly={readOnly || possibleOptions.length <= 1} searchable={projects.length > 10} - disabled={props.disabled} + disabled={disabled} loading={projectsLoader.isLoading()} - inline={props.inline} + inline={inline} tiny fill onSelect={handleProjectSelect} > - {translate('connections_connection_project')} + {translate('plugin_projects_project_select_label')} ); }); \ No newline at end of file diff --git a/webapp/packages/plugin-projects/src/index.ts b/webapp/packages/plugin-projects/src/index.ts new file mode 100644 index 0000000000..7211905f88 --- /dev/null +++ b/webapp/packages/plugin-projects/src/index.ts @@ -0,0 +1,5 @@ +import { manifest } from './manifest'; +export * from './FolderDialog'; +export * from './ProjectSelect'; + +export default manifest; \ No newline at end of file diff --git a/webapp/packages/plugin-projects/src/locales/en.ts b/webapp/packages/plugin-projects/src/locales/en.ts new file mode 100644 index 0000000000..08a24cdde4 --- /dev/null +++ b/webapp/packages/plugin-projects/src/locales/en.ts @@ -0,0 +1,3 @@ +export default [ + ['plugin_projects_project_select_label', 'Project'], +]; diff --git a/webapp/packages/plugin-projects/src/locales/it.ts b/webapp/packages/plugin-projects/src/locales/it.ts new file mode 100644 index 0000000000..08a24cdde4 --- /dev/null +++ b/webapp/packages/plugin-projects/src/locales/it.ts @@ -0,0 +1,3 @@ +export default [ + ['plugin_projects_project_select_label', 'Project'], +]; diff --git a/webapp/packages/plugin-projects/src/locales/ru.ts b/webapp/packages/plugin-projects/src/locales/ru.ts new file mode 100644 index 0000000000..774a04bef4 --- /dev/null +++ b/webapp/packages/plugin-projects/src/locales/ru.ts @@ -0,0 +1,3 @@ +export default [ + ['plugin_projects_project_select_label', 'Проект'], +]; diff --git a/webapp/packages/plugin-projects/src/locales/zh.ts b/webapp/packages/plugin-projects/src/locales/zh.ts new file mode 100644 index 0000000000..08a24cdde4 --- /dev/null +++ b/webapp/packages/plugin-projects/src/locales/zh.ts @@ -0,0 +1,3 @@ +export default [ + ['plugin_projects_project_select_label', 'Project'], +]; diff --git a/webapp/packages/plugin-projects/src/manifest.ts b/webapp/packages/plugin-projects/src/manifest.ts new file mode 100644 index 0000000000..05448d19bc --- /dev/null +++ b/webapp/packages/plugin-projects/src/manifest.ts @@ -0,0 +1,23 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 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 type { PluginManifest } from '@cloudbeaver/core-di'; + +import { LocaleService } from './LocaleService'; +import { PluginBootstrap } from './PluginBootstrap'; + +export const manifest: PluginManifest = { + info: { + name: 'Projects plugin', + }, + + providers: [ + PluginBootstrap, + LocaleService, + ], +}; diff --git a/webapp/packages/plugin-projects/tsconfig.json b/webapp/packages/plugin-projects/tsconfig.json new file mode 100644 index 0000000000..2a4cc370f7 --- /dev/null +++ b/webapp/packages/plugin-projects/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "composite": true + }, + "include": ["src"], + "exclude": ["**/node_modules", "**/.*/"], + "references": [{ "path": "../*" }] +} diff --git a/webapp/packages/plugin-resource-manager/src/NavNodes/ResourceFoldersBootstrap.ts b/webapp/packages/plugin-resource-manager/src/NavNodes/ResourceFoldersBootstrap.ts index 11746be06f..16f5a643d5 100644 --- a/webapp/packages/plugin-resource-manager/src/NavNodes/ResourceFoldersBootstrap.ts +++ b/webapp/packages/plugin-resource-manager/src/NavNodes/ResourceFoldersBootstrap.ts @@ -11,15 +11,17 @@ import { untracked } from 'mobx'; import { UserInfoResource } from '@cloudbeaver/core-authentication'; import { CONNECTION_FOLDER_NAME_VALIDATION } from '@cloudbeaver/core-connections'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { RenameDialog, DialogueStateResult, CommonDialogService } from '@cloudbeaver/core-dialogs'; +import { DialogueStateResult, CommonDialogService } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { LocalizationService } from '@cloudbeaver/core-localization'; import { NavTreeResource, NavNodeManagerService, NavNodeInfoResource, type INodeMoveData, navNodeMoveContext, getNodesFromContext, ENodeMoveType, type NavNode } from '@cloudbeaver/core-navigation-tree'; -import { ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-sdk'; +import { ProjectInfoResource } from '@cloudbeaver/core-projects'; +import { CachedMapAllKey, ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-sdk'; import { createPath } from '@cloudbeaver/core-utils'; import { ActionService, MenuService, ACTION_NEW_FOLDER, DATA_CONTEXT_MENU, IAction, IDataContextProvider } from '@cloudbeaver/core-view'; import { DATA_CONTEXT_ELEMENTS_TREE, MENU_ELEMENTS_TREE_TOOLS, type IElementsTree } from '@cloudbeaver/plugin-navigation-tree'; +import { FolderDialog } from '@cloudbeaver/plugin-projects'; import { NAV_NODE_TYPE_RM_PROJECT } from '../NAV_NODE_TYPE_RM_PROJECT'; import { NavResourceNodeService } from '../NavResourceNodeService'; @@ -27,6 +29,15 @@ import { ResourceManagerResource } from '../ResourceManagerResource'; import { ResourceProjectsResource } from '../ResourceProjectsResource'; import { RESOURCES_NODE_PATH } from '../RESOURCES_NODE_PATH'; import { NAV_NODE_TYPE_RM_RESOURCE } from './NAV_NODE_TYPE_RM_RESOURCE'; +import { ResourcesProjectsNavNodeService } from './ResourcesProjectsNavNodeService'; + +interface ITargetNode { + projectId: string; + folderId?: string; + + projectNodeId: string; + selectProject: boolean; +} @injectable() export class ResourceFoldersBootstrap extends Bootstrap { @@ -40,10 +51,12 @@ export class ResourceFoldersBootstrap extends Bootstrap { private readonly navResourceNodeService: NavResourceNodeService, private readonly resourceManagerResource: ResourceManagerResource, private readonly resourceProjectsResource: ResourceProjectsResource, + private readonly projectInfoResource: ProjectInfoResource, private readonly commonDialogService: CommonDialogService, private readonly actionService: ActionService, private readonly menuService: MenuService, - private readonly navNodeInfoResource: NavNodeInfoResource + private readonly navNodeInfoResource: NavNodeInfoResource, + private readonly resourcesProjectsNavNodeService: ResourcesProjectsNavNodeService ) { super(); } @@ -104,32 +117,25 @@ export class ResourceFoldersBootstrap extends Bootstrap { const move = contexts.getContext(navNodeMoveContext); const nodes = getNodesFromContext(moveContexts); const nodeIdList = nodes.map(node => node.id); - const children = this.navTreeResource.get(targetNode.id); - const data = this.navResourceNodeService.getResourceData(targetNode.id); + const children = this.navTreeResource.get(targetNode.id) ?? []; + const targetProject = this.resourcesProjectsNavNodeService.getProject(targetNode.id); - if (!data) { + if (!targetProject?.canEditResources || (!targetNode.folder && targetNode.nodeType !== NAV_NODE_TYPE_RM_PROJECT)) { return; } - await this.resourceProjectsResource.load(); - const projectPath = createPath(RESOURCES_NODE_PATH, this.resourceProjectsResource.userProject?.id); + const supported = nodes.every(node => { + if ( + ![NAV_NODE_TYPE_RM_PROJECT, NAV_NODE_TYPE_RM_RESOURCE].includes(node.nodeType!) + || targetProject !== this.resourcesProjectsNavNodeService.getProject(node.id) + || children.includes(node.id) + || targetNode.id === node.id + ) { + return false; + } - if (!(this.resourceProjectsResource.userProject?.id === data.key.projectId)) { - return; - } - - const supported = ( - ( - [NAV_NODE_TYPE_RM_PROJECT, NAV_NODE_TYPE_RM_RESOURCE].includes(targetNode.nodeType!) - || targetNode.id === projectPath - ) - && (targetNode.folder || NAV_NODE_TYPE_RM_PROJECT === targetNode.nodeType) - && nodes.every(node => ( - node.nodeType === NAV_NODE_TYPE_RM_RESOURCE - && node.id !== targetNode.id - && !children?.includes(node.id) - )) - ); + return true; + }); if (!supported) { return; @@ -140,7 +146,7 @@ export class ResourceFoldersBootstrap extends Bootstrap { } else { try { await this.navTreeResource.moveTo(resourceKeyList(nodeIdList), targetNode.id); - await this.navTreeResource.refreshTree(RESOURCES_NODE_PATH); + await this.navTreeResource.refreshTree(RESOURCES_NODE_PATH, true); } catch (exception: any) { this.notificationService.logException(exception, 'plugin_resource_manager_folder_move_failed'); } @@ -164,51 +170,49 @@ export class ResourceFoldersBootstrap extends Bootstrap { return; } - const folderData = this.navResourceNodeService.getResourceData(targetNode.id); + let parentFolder: string | undefined; - if (!folderData) { - return; + if (targetNode.folderId) { + const folderData = this.navResourceNodeService.getResourceData(targetNode.folderId); + + if (folderData) { + parentFolder = folderData.resourcePath; + } } - let { key } = folderData; + await this.resourceManagerResource.load({ projectId: targetNode.projectId, folder: parentFolder }); - await this.resourceManagerResource.load(key); - const resourceData = this.resourceManagerResource.getResource(key, folderData.name); - - if (resourceData?.folder) { - key = { ...key, folder: createPath(key.folder, resourceData.name) }; - await this.resourceManagerResource.load(key); - } - - const result = await this.commonDialogService.open(RenameDialog, { + const result = await this.commonDialogService.open(FolderDialog, { value: this.localizationService.translate('ui_folder_new'), + projectId: targetNode.projectId, title: 'core_view_action_new_folder', - subTitle: key.folder, + subTitle: parentFolder, icon: '/icons/folder.svg#root', create: true, - validation: (name, setMessage) => { - const trimmed = name.trim(); + selectProject: targetNode.selectProject, + validation: async ({ folder, projectId }, setMessage) => { + const trimmed = folder.trim(); - if (trimmed.length === 0 || !name.match(CONNECTION_FOLDER_NAME_VALIDATION)) { + if (trimmed.length === 0 || !folder.match(CONNECTION_FOLDER_NAME_VALIDATION)) { setMessage('connections_connection_folder_validation'); return false; } - const folder = this.resourceManagerResource.getResource(key, trimmed); + await this.resourceManagerResource.load({ projectId: projectId, folder: parentFolder }); - return folder === undefined; + return !this.resourceManagerResource.hasResource({ projectId: projectId, folder: parentFolder }, trimmed); }, }); if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) { try { await this.resourceManagerResource.createResource( - key.projectId, - createPath(key.folder, result), + result.projectId, + createPath(parentFolder, result.folder), true ); - this.navTreeResource.refreshTree(targetNode.parentId); + this.navTreeResource.refreshTree(this.resourcesProjectsNavNodeService.getProjectNodeId(result.projectId)); } catch (exception: any) { this.notificationService.logException(exception, 'Error occurred while renaming'); } @@ -230,26 +234,51 @@ export class ResourceFoldersBootstrap extends Bootstrap { } } - private getTargetNode(tree: IElementsTree): NavNode | undefined { + private getTargetNode(tree: IElementsTree): ITargetNode | undefined { + untracked(() => this.projectInfoResource.load(CachedMapAllKey)); const selected = tree.getSelected(); if (selected.length === 0) { - selected.push(tree.root); + const editableProjects = this.projectInfoResource.values.filter(project => project.canEditResources); + + if (editableProjects.length > 0) { + const project = editableProjects[0]; + + return { + projectId: project.id, + projectNodeId: this.resourcesProjectsNavNodeService.getProjectNodeId(project.id), + selectProject: editableProjects.length > 1, + }; + } + return; } - let targetFolder = selected[0]; - let targetNode = this.navNodeInfoResource.get(targetFolder); + const targetFolder = selected[0]; + const parentIds = [...this.navNodeInfoResource.getParents(targetFolder), targetFolder]; + const parents = this.navNodeInfoResource.get(resourceKeyList(parentIds)); + const projectNode = parents.find(parent => parent?.nodeType === NAV_NODE_TYPE_RM_PROJECT); - if (![NAV_NODE_TYPE_RM_PROJECT, NAV_NODE_TYPE_RM_RESOURCE].includes(targetNode?.nodeType as any)) { - targetFolder = tree.baseRoot; - targetNode = this.navNodeInfoResource.get(targetFolder); + if (!projectNode) { + return; } - const projectPath = createPath(RESOURCES_NODE_PATH, this.resourceProjectsResource.userProject?.id); - if (!targetNode?.id.startsWith(projectPath)) { - targetNode = undefined; + + const project = this.resourcesProjectsNavNodeService.getByNodeId(projectNode.id); + + if (!project?.canEditResources) { + return; } - return targetNode; + const targetFolderNode = parents + .slice() + .reverse() + .find(parent => parent?.nodeType === NAV_NODE_TYPE_RM_RESOURCE && parent.folder); + + return { + projectId: project.id, + folderId: targetFolderNode?.id, + projectNodeId: projectNode.id, + selectProject: false, + }; } } \ No newline at end of file diff --git a/webapp/packages/plugin-resource-manager/src/NavNodes/ResourcesProjectsNavNodeService.ts b/webapp/packages/plugin-resource-manager/src/NavNodes/ResourcesProjectsNavNodeService.ts new file mode 100644 index 0000000000..a04dbad1e8 --- /dev/null +++ b/webapp/packages/plugin-resource-manager/src/NavNodes/ResourcesProjectsNavNodeService.ts @@ -0,0 +1,47 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 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 { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree'; +import { ProjectInfo, ProjectInfoResource } from '@cloudbeaver/core-projects'; +import { resourceKeyList } from '@cloudbeaver/core-sdk'; +import { createPath } from '@cloudbeaver/core-utils'; + +import { NAV_NODE_TYPE_RM_PROJECT } from '../NAV_NODE_TYPE_RM_PROJECT'; +import { RESOURCES_NODE_PATH } from '../RESOURCES_NODE_PATH'; + +@injectable() +export class ResourcesProjectsNavNodeService { + + constructor( + private readonly navNodeInfoResource: NavNodeInfoResource, + private readonly projectInfoResource: ProjectInfoResource + ) { + } + + getProject(nodeId: string): ProjectInfo | undefined { + const parentIds = [...this.navNodeInfoResource.getParents(nodeId), nodeId]; + const parents = this.navNodeInfoResource.get(resourceKeyList(parentIds)); + + const projectNode = parents.find(parent => parent?.nodeType === NAV_NODE_TYPE_RM_PROJECT); + + if (!projectNode) { + return; + } + + return this.getByNodeId(projectNode.id); + } + + getByNodeId(nodeId: string): ProjectInfo | undefined { + return this.projectInfoResource.get(nodeId.replace(RESOURCES_NODE_PATH + '/', '')); + } + + getProjectNodeId(projectId: string): string { + return createPath(RESOURCES_NODE_PATH, projectId); + } +} \ No newline at end of file diff --git a/webapp/packages/plugin-resource-manager/src/PluginBootstrap.ts b/webapp/packages/plugin-resource-manager/src/PluginBootstrap.ts index 0dc6b9626c..8d739ef712 100644 --- a/webapp/packages/plugin-resource-manager/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-resource-manager/src/PluginBootstrap.ts @@ -7,16 +7,9 @@ */ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { CommonDialogService, ConfirmationDialogDelete, DialogueStateResult } from '@cloudbeaver/core-dialogs'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { LocalizationService } from '@cloudbeaver/core-localization'; -import { DATA_CONTEXT_NAV_NODE } from '@cloudbeaver/core-navigation-tree'; import { SideBarPanelService } from '@cloudbeaver/core-ui'; -import { ActionService, ACTION_DELETE, MenuService } from '@cloudbeaver/core-view'; -import { isScript } from '@cloudbeaver/plugin-sql-editor-navigation-tab-resource'; import { EMainMenu, MainMenuService } from '@cloudbeaver/plugin-top-app-bar'; -import { NavResourceNodeService, PROJECT_NODE_TYPE, RESOURCE_NODE_TYPE } from './NavResourceNodeService'; import { ResourceManager } from './ResourceManager'; import { ResourceManagerService } from './ResourceManagerService'; @@ -26,12 +19,6 @@ export class PluginBootstrap extends Bootstrap { private readonly mainMenuService: MainMenuService, private readonly resourceManagerService: ResourceManagerService, private readonly sideBarPanelService: SideBarPanelService, - private readonly navResourceNodeService: NavResourceNodeService, - private readonly notificationService: NotificationService, - private readonly commonDialogService: CommonDialogService, - private readonly actionService: ActionService, - private readonly menuService: MenuService, - private readonly localizationService: LocalizationService, ) { super(); } @@ -58,56 +45,6 @@ export class PluginBootstrap extends Bootstrap { onClose: this.resourceManagerService.togglePanel, panel: () => ResourceManager, }); - - this.actionService.addHandler({ - id: 'resource-manager-base-actions', - isActionApplicable: (context, action) => { - if (!context.has(DATA_CONTEXT_NAV_NODE)) { - return false; - } - - const node = context.get(DATA_CONTEXT_NAV_NODE); - - if (action === ACTION_DELETE) { - return isScript(node.id); - } - - return false; - }, - handler: async (context, action) => { - const node = context.get(DATA_CONTEXT_NAV_NODE); - - if (action === ACTION_DELETE) { - const result = await this.commonDialogService.open(ConfirmationDialogDelete, { - title: 'ui_data_delete_confirmation', - message: this.localizationService.translate('plugin_resource_manager_script_delete_confirmation', undefined, { name: node.name }), - confirmActionText: 'ui_delete', - }); - - if (result === DialogueStateResult.Resolved) { - try { - const resourceData = this.navResourceNodeService.getResourceData(node.id); - - if (!resourceData) { - throw new Error('Can\'t find resource'); - } - - await this.navResourceNodeService.delete(resourceData); - } catch (exception: any) { - this.notificationService.logException(exception, 'plugin_resource_manager_delete_script_error'); - } - } - } - }, - }); - - this.menuService.addCreator({ - isApplicable: context => { - const node = context.tryGet(DATA_CONTEXT_NAV_NODE); - return !!node?.nodeType && [PROJECT_NODE_TYPE, RESOURCE_NODE_TYPE].includes(node.nodeType); - }, - getItems: (context, items) => items, - }); } async load(): Promise { } diff --git a/webapp/packages/plugin-resource-manager/src/ResourceManagerResource.ts b/webapp/packages/plugin-resource-manager/src/ResourceManagerResource.ts index 7459f92ec5..dd6002397b 100644 --- a/webapp/packages/plugin-resource-manager/src/ResourceManagerResource.ts +++ b/webapp/packages/plugin-resource-manager/src/ResourceManagerResource.ts @@ -28,6 +28,12 @@ export class ResourceManagerResource extends CachedMapResource resource.name === resourcePath) === true; + } + getResource(key: IResourceManagerParams, resourcePath: string): RmResourceInfo | undefined { const resources = this.get(key); diff --git a/webapp/packages/plugin-resource-manager/src/SaveScriptDialog.tsx b/webapp/packages/plugin-resource-manager/src/SaveScriptDialog.tsx index ab18bf5d9b..692a2a22d1 100644 --- a/webapp/packages/plugin-resource-manager/src/SaveScriptDialog.tsx +++ b/webapp/packages/plugin-resource-manager/src/SaveScriptDialog.tsx @@ -11,12 +11,10 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; import { BASE_CONTAINERS_STYLES, Button, Container, InputField, SubmittingForm, useFocus, useObservableRef } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; import { CommonDialogWrapper, DialogComponent } from '@cloudbeaver/core-dialogs'; import { Translate, useTranslate } from '@cloudbeaver/core-localization'; -import { ProjectsService, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects'; import { useStyles } from '@cloudbeaver/core-theming'; -import { ProjectSelect } from '@cloudbeaver/plugin-connections/src/ConnectionForm/Options/ProjectSelect'; +import { ProjectSelect } from '@cloudbeaver/plugin-projects'; const style = css` fill { @@ -30,7 +28,7 @@ interface Payload { export interface ISaveScriptDialogResult { name: string; - projectId: string; + projectId: string | null; } interface State extends ISaveScriptDialogResult { @@ -49,11 +47,10 @@ export const SaveScriptDialog: DialogComponent }) { const translate = useTranslate(); const [focusedRef] = useFocus({ focusFirstChild: true }); - const projectsService = useService(ProjectsService); const state = useObservableRef(() => ({ name: payload.defaultScriptName ?? '', - projectId: projectsService.activeProject?.id ?? PROJECT_GLOBAL_ID, + projectId: null, errorMessage: null, validate() { this.errorMessage = null; @@ -99,7 +96,7 @@ export const SaveScriptDialog: DialogComponent + {!data.readonly && ( + + )} - {!data.activeSegmentMode.activeSegmentMode && ( + {!data.activeSegmentMode.activeSegmentMode && !data.readonly && (