diff --git a/.gitignore b/.gitignore index 8906c71da1..93b1d9e73b 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,9 @@ server/test/io.cloudbeaver.test.platform/workspace/.data/ .classpath .settings/ +## Eclipse PDE +*.product.launch + workspace-dev-ce/ deploy/cloudbeaver server/**/target diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 96fcdd0661..3fef97a54a 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -55,10 +55,10 @@ "label": "Build CE", "type": "shell", "windows": { - "command": "./build-sqlite.bat" + "command": "./build.bat" }, "osx": { - "command": "./build-sqlite.sh" + "command": "./build.sh" }, "options": { "cwd": "${workspaceFolder}/deploy" diff --git a/README.md b/README.md index 0b4f37887f..cce2fd8ea8 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,13 @@ You can see a live demo of CloudBeaver here: https://demo.cloudbeaver.io ## Changelog +### 24.2.2. 2024-10-07 +- Schemas were added to the SQL autocompletion for PostgreSQL, H2, and SQL Server; +- CloudBeaver can now correctly display negative dates for MySQL database; +- A search option was added for preferences in the Administration part; +- Keyboard navigation has been enhanced. You can now use the arrow keys to move through navigator tree elements and the tab key to switch between editors tabs; +- Sample SQLite database was removed. + ### 24.2.1. 2024-09-23 - Chinese localization has been improved (thanks to [cashlifei](https://github.com/cashlifei)); - Environment variables configuration has been improved - now you can configure more variables on the initial stage of the Docker setup; diff --git a/SECURITY.md b/SECURITY.md index 9d72644c1e..094ed1ba00 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -9,9 +9,10 @@ currently being supported with security updates. | ------- | --------- | | 22.x | yes | | 23.x | yes | +| 24.x | yes | ## Reporting a Vulnerability -Please report (suspected) security vulnerabilities to devops@dbeaver.com. -You will receive a response from us within 48 hours. -If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days. +Please report (suspected) security vulnerabilities to devops@dbeaver.com. +You will receive a response from us within 48 hours. +If the issue is confirmed, we will release a patch as soon as possible, depending on complexity, but historically, within a few days. diff --git a/config/GlobalConfiguration/.dbeaver/data-sources.json b/config/GlobalConfiguration/.dbeaver/data-sources.json index c954ec82d9..a5f18e204f 100644 --- a/config/GlobalConfiguration/.dbeaver/data-sources.json +++ b/config/GlobalConfiguration/.dbeaver/data-sources.json @@ -1,24 +1,4 @@ { - "folders": {}, - "connections": { - "postgresql-template-1": { - "provider": "postgresql", - "driver": "postgres-jdbc", - "name": "PostgreSQL (Template)", - "save-password": false, - "show-system-objects": false, - "read-only": true, - "template": true, - "configuration": { - "host": "localhost", - "port": "5432", - "database": "postgres", - "url": "jdbc:postgresql://localhost:5432/postgres", - "type": "dev", - "provider-properties": { - "@dbeaver-show-non-default-db@": "false" - } - } - } - } + "folders": {}, + "connections": {} } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/BaseWebProjectImpl.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/BaseWebProjectImpl.java index fb066b6719..ddbd377c4e 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/BaseWebProjectImpl.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/BaseWebProjectImpl.java @@ -23,7 +23,6 @@ import org.jkiss.dbeaver.model.rm.RMController; import org.jkiss.dbeaver.model.rm.RMControllerProvider; import org.jkiss.dbeaver.model.rm.RMProject; -import org.jkiss.dbeaver.model.rm.RMUtils; import org.jkiss.utils.CommonUtils; import org.jkiss.utils.Pair; @@ -44,11 +43,12 @@ public BaseWebProjectImpl( @NotNull DBPWorkspace workspace, @NotNull RMController resourceController, @NotNull SMSessionContext sessionContext, - @NotNull RMProject project + @NotNull RMProject project, + @NotNull Path path ) { super(workspace, sessionContext); this.resourceController = resourceController; - this.path = RMUtils.getProjectPath(project); + this.path = path; this.project = project; } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebHeadlessSessionProjectImpl.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebHeadlessSessionProjectImpl.java index 10dfc6acce..852520ef2f 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebHeadlessSessionProjectImpl.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebHeadlessSessionProjectImpl.java @@ -19,6 +19,7 @@ import io.cloudbeaver.model.session.WebHeadlessSession; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.model.rm.RMProject; +import org.jkiss.dbeaver.model.rm.RMUtils; public class WebHeadlessSessionProjectImpl extends WebProjectImpl { public WebHeadlessSessionProjectImpl( @@ -30,7 +31,8 @@ public WebHeadlessSessionProjectImpl( session.getUserContext().getRmController(), session.getSessionContext(), project, - session.getUserContext().getPreferenceStore() + session.getUserContext().getPreferenceStore(), + RMUtils.getProjectPath(project) ); } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebProjectImpl.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebProjectImpl.java index 00a4c64136..c6c7144198 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebProjectImpl.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebProjectImpl.java @@ -30,18 +30,22 @@ import org.jkiss.dbeaver.registry.rm.DataSourceRegistryRM; import org.jkiss.dbeaver.runtime.DBWorkbench; +import java.nio.file.Path; + public abstract class WebProjectImpl extends BaseWebProjectImpl { private static final Log log = Log.getLog(WebProjectImpl.class); @NotNull protected final DBPPreferenceStore preferenceStore; + public WebProjectImpl( @NotNull DBPWorkspace workspace, @NotNull RMController resourceController, @NotNull SMSessionContext sessionContext, @NotNull RMProject project, - @NotNull DBPPreferenceStore preferenceStore + @NotNull DBPPreferenceStore preferenceStore, + @NotNull Path path ) { - super(workspace, resourceController, sessionContext, project); + super(workspace, resourceController, sessionContext, project, path); this.preferenceStore = preferenceStore; } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebSessionProjectImpl.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebSessionProjectImpl.java index 5dacda453f..b55860c05b 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebSessionProjectImpl.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebSessionProjectImpl.java @@ -27,10 +27,12 @@ import org.jkiss.dbeaver.model.app.DBPDataSourceRegistryCache; import org.jkiss.dbeaver.model.navigator.DBNModel; import org.jkiss.dbeaver.model.rm.RMProject; +import org.jkiss.dbeaver.model.rm.RMUtils; import org.jkiss.dbeaver.model.websocket.event.WSEventType; import org.jkiss.dbeaver.registry.DataSourceDescriptor; import org.jkiss.dbeaver.runtime.jobs.DisconnectJob; +import java.nio.file.Path; import java.util.*; import java.util.stream.Collectors; @@ -49,7 +51,24 @@ public WebSessionProjectImpl( webSession.getRmController(), webSession.getSessionContext(), project, - webSession.getUserPreferenceStore() + webSession.getUserPreferenceStore(), + RMUtils.getProjectPath(project) + ); + this.webSession = webSession; + } + + public WebSessionProjectImpl( + @NotNull WebSession webSession, + @NotNull RMProject project, + @NotNull Path path + ) { + super( + webSession.getWorkspace(), + webSession.getRmController(), + webSession.getSessionContext(), + project, + webSession.getUserPreferenceStore(), + path ); this.webSession = webSession; } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseServerConfigurationController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseServerConfigurationController.java index 516ef29e2a..781f904e81 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseServerConfigurationController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseServerConfigurationController.java @@ -19,19 +19,86 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.registry.fs.FileSystemProviderRegistry; +import org.jkiss.utils.IOUtils; + +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Path; /** * Abstract class that contains methods for loading configuration with gson. */ public abstract class BaseServerConfigurationController implements WebServerConfigurationController { + private static final Log log = Log.getLog(BaseServerConfigurationController.class); + @NotNull + private final Path homeDirectory; + + protected Path workspacePath; + + protected BaseServerConfigurationController(@NotNull Path homeDirectory) { + this.homeDirectory = homeDirectory; + //default workspaceLocation + this.workspacePath = homeDirectory.resolve("workspace"); + } @NotNull public Gson getGson() { return getGsonBuilder().create(); } + @NotNull protected abstract GsonBuilder getGsonBuilder(); public abstract T getServerConfiguration(); + + + @NotNull + protected synchronized void initWorkspacePath() throws DBException { + if (workspacePath != null && !IOUtils.isFileFromDefaultFS(workspacePath)) { + log.warn("Workspace directory already initialized: " + workspacePath); + } + String workspaceLocation = getWorkspaceLocation(); + URI workspaceUri = URI.create(workspaceLocation); + if (workspaceUri.getScheme() == null) { + // default filesystem + this.workspacePath = getHomeDirectory().resolve(workspaceLocation); + } else { + var externalFsProvider = + FileSystemProviderRegistry.getInstance().getFileSystemProviderBySchema(workspaceUri.getScheme()); + if (externalFsProvider == null) { + throw new DBException("File system not found for scheme: " + workspaceUri.getScheme()); + } + ClassLoader fsClassloader = externalFsProvider.getInstance().getClass().getClassLoader(); + try (FileSystem externalFileSystem = FileSystems.newFileSystem(workspaceUri, + System.getenv(), + fsClassloader);) { + this.workspacePath = externalFileSystem.provider().getPath(workspaceUri); + } catch (Exception e) { + throw new DBException("Failed to initialize workspace path: " + workspaceUri, e); + } + } + log.info("Workspace path initialized: " + workspacePath); + } + + @NotNull + protected abstract String getWorkspaceLocation(); + + @NotNull + protected Path getHomeDirectory() { + return homeDirectory; + } + + @NotNull + @Override + public Path getWorkspacePath() { + if (workspacePath == null) { + throw new RuntimeException("Workspace path not initialized"); + } + return workspacePath; + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java index 9483767290..87f9b85409 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java @@ -233,6 +233,12 @@ public String getWorkspaceIdProperty() throws DBException { return BaseWorkspaceImpl.readWorkspaceIdProperty(); } + @Override + public Path getWorkspaceDirectory() { + return getServerConfigurationController().getWorkspacePath(); + } + + public String getApplicationId() { try { return getApplicationInstanceId(); @@ -252,4 +258,12 @@ public WSEventController getEventController() { public boolean isEnvironmentVariablesAccessible() { return false; } + + protected void closeResource(String name, Runnable closeFunction) { + try { + closeFunction.run(); + } catch (Exception e) { + log.error("Failed close " + name, e); + } + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAppConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAppConfiguration.java index 92c2388921..ca09529a49 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAppConfiguration.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAppConfiguration.java @@ -17,6 +17,7 @@ package io.cloudbeaver.model.app; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import java.util.Map; @@ -28,6 +29,7 @@ public interface WebAppConfiguration { boolean isAnonymousAccessEnabled(); + @Nullable T getResourceQuota(String quotaId); String getDefaultUserTeam(); diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfiguration.java index 1c8fa96c2c..82cbfeb05a 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfiguration.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfiguration.java @@ -16,6 +16,11 @@ */ package io.cloudbeaver.model.app; +import io.cloudbeaver.server.WebServerPreferenceStore; +import org.jkiss.code.NotNull; + +import java.util.Map; + /** * Web server configuration. * Contains only server configuration properties. @@ -27,4 +32,12 @@ default String getRootURI() { return ""; } + /** + * @return the setting values that will be used in {@link WebServerPreferenceStore} + */ + @NotNull + default Map getProductSettings() { + return Map.of(); + } + } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfigurationController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfigurationController.java index c06f137b70..f6823f6dfb 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfigurationController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfigurationController.java @@ -39,6 +39,11 @@ default Map getOriginalConfigurationProperties() { return Map.of(); } + @NotNull + Path getWorkspacePath(); + @NotNull Gson getGson(); + + void validateFinalServerConfiguration() throws DBException; } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/BaseLocalResourceController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/BaseLocalResourceController.java new file mode 100644 index 0000000000..fbea3749e0 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/BaseLocalResourceController.java @@ -0,0 +1,352 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.model.rm.local; + +import io.cloudbeaver.BaseWebProjectImpl; +import io.cloudbeaver.model.rm.lock.RMFileLockController; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPDataSourceConfigurationStorage; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.DBPDataSourceFolder; +import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; +import org.jkiss.dbeaver.model.app.DBPProject; +import org.jkiss.dbeaver.model.app.DBPWorkspace; +import org.jkiss.dbeaver.model.impl.auth.SessionContextImpl; +import org.jkiss.dbeaver.model.rm.RMController; +import org.jkiss.dbeaver.model.rm.RMEvent; +import org.jkiss.dbeaver.model.rm.RMEventManager; +import org.jkiss.dbeaver.model.rm.RMProject; +import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor; +import org.jkiss.dbeaver.registry.*; +import org.jkiss.dbeaver.utils.GeneralUtils; +import org.jkiss.utils.ArrayUtils; +import org.jkiss.utils.IOUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.function.Predicate; + +public abstract class BaseLocalResourceController implements RMController { + private static final Log log = Log.getLog(BaseLocalResourceController.class); + + public static final String DEFAULT_CHANGE_ID = "0"; + private static final String FILE_REGEX = "(?U)[\\w.$()@/\\\\ -]+"; + private static final String PROJECT_REGEX = "(?U)[\\w.$()@ -]+"; // slash not allowed in project name + + @NotNull + protected final DBPWorkspace workspace; + @NotNull + protected final RMFileLockController lockController; + + protected BaseLocalResourceController( + @NotNull DBPWorkspace workspace, + @NotNull RMFileLockController lockController + ) { + this.workspace = workspace; + this.lockController = lockController; + } + + @Override + public RMProject getProject(@NotNull String projectId, boolean readResources, boolean readProperties) + throws DBException { + RMProject project = makeProjectFromId(projectId, true); + if (project == null) { + return null; + } + if (readResources) { + doProjectOperation(projectId, () -> { + project.setChildren( + listResources(projectId, null, null, readProperties, false, true) + ); + return null; + }); + } + return project; + } + + @Override + public Object getProjectProperty(@NotNull String projectId, @NotNull String propName) throws DBException { + var project = getWebProject(projectId, false); + return doFileReadOperation(projectId, + project.getMetadataFilePath(), + () -> project.getProjectProperty(propName)); + } + + @Override + public void setProjectProperty( + @NotNull String projectId, + @NotNull String propName, + @NotNull Object propValue + ) throws DBException { + BaseWebProjectImpl webProject = getWebProject(projectId, false); + doFileWriteOperation(projectId, webProject.getMetadataFilePath(), + () -> { + log.debug("Updating value for property '" + propName + "' in project '" + projectId + "'"); + webProject.setProjectProperty(propName, propValue); + return null; + } + ); + } + + @Override + public String getProjectsDataSources(@NotNull String projectId, @Nullable String[] dataSourceIds) + throws DBException { + DBPProject projectMetadata = getWebProject(projectId, false); + return doFileReadOperation( + projectId, + projectMetadata.getMetadataFolder(false), + () -> { + DBPDataSourceRegistry registry = projectMetadata.getDataSourceRegistry(); + registry.refreshConfig(); + registry.checkForErrors(); + DataSourceConfigurationManagerBuffer buffer = new DataSourceConfigurationManagerBuffer(); + Predicate filter = null; + if (!ArrayUtils.isEmpty(dataSourceIds)) { + filter = ds -> ArrayUtils.contains(dataSourceIds, ds.getId()); + } + ((DataSourcePersistentRegistry) registry).saveConfigurationToManager(new VoidProgressMonitor(), + buffer, + filter); + registry.checkForErrors(); + + return new String(buffer.getData(), StandardCharsets.UTF_8); + } + ); + } + + @Override + public void createProjectDataSources( + @NotNull String projectId, + @NotNull String configuration, + @Nullable List dataSourceIds + ) throws DBException { + updateProjectDataSources(projectId, configuration, dataSourceIds); + } + + @Override + public boolean updateProjectDataSources( + @NotNull String projectId, + @NotNull String configuration, + @Nullable List dataSourceIds + ) throws DBException { + try (var lock = lockController.lockProject(projectId, "updateProjectDataSources")) { + DBPProject project = getWebProject(projectId, false); + return doFileWriteOperation(projectId, project.getMetadataFolder(false), + () -> { + DBPDataSourceRegistry registry = project.getDataSourceRegistry(); + DBPDataSourceConfigurationStorage storage = new DataSourceMemoryStorage(configuration.getBytes( + StandardCharsets.UTF_8)); + DataSourceConfigurationManager manager = new DataSourceConfigurationManagerBuffer(); + var configChanged = ((DataSourcePersistentRegistry) registry).loadDataSources( + List.of(storage), + manager, + dataSourceIds, + true, + false + ); + registry.checkForErrors(); + log.debug("Save data sources configuration in project '" + projectId + "'"); + ((DataSourcePersistentRegistry) registry).saveDataSources(); + registry.checkForErrors(); + return configChanged; + } + ); + } + } + + @Override + public void deleteProjectDataSources( + @NotNull String projectId, + @NotNull String[] dataSourceIds + ) throws DBException { + try (var projectLock = lockController.lockProject(projectId, "deleteDatasources")) { + DBPProject project = getWebProject(projectId, false); + doFileWriteOperation(projectId, project.getMetadataFolder(false), () -> { + DBPDataSourceRegistry registry = project.getDataSourceRegistry(); + for (String dataSourceId : dataSourceIds) { + DBPDataSourceContainer dataSource = registry.getDataSource(dataSourceId); + + if (dataSource != null) { + log.debug("Deleting data source '" + dataSourceId + "' in project '" + projectId + "'"); + registry.removeDataSource(dataSource); + } else { + log.warn("Could not find datasource " + dataSourceId + " for deletion"); + } + } + registry.checkForErrors(); + return null; + }); + } + } + + @Override + public void createProjectDataSourceFolder( + @NotNull String projectId, + @NotNull String folderPath + ) throws DBException { + try (var projectLock = lockController.lockProject(projectId, "createDatasourceFolder")) { + DBPProject project = getWebProject(projectId, false); + log.debug("Creating data source folder '" + folderPath + "' in project '" + projectId + "'"); + doFileWriteOperation(projectId, project.getMetadataFolder(false), + () -> { + DBPDataSourceRegistry registry = project.getDataSourceRegistry(); + var result = Path.of(folderPath); + var newName = result.getFileName().toString(); + GeneralUtils.validateResourceName(newName); + var parent = result.getParent(); + var parentFolder = parent == null ? null : registry.getFolder(parent.toString().replace("\\", "/")); + DBPDataSourceFolder newFolder = registry.addFolder(parentFolder, newName); + registry.checkForErrors(); + return null; + } + ); + } + } + + @Override + public void deleteProjectDataSourceFolders( + @NotNull String projectId, + @NotNull String[] folderPaths, + boolean dropContents + ) throws DBException { + try (var projectLock = lockController.lockProject(projectId, "createDatasourceFolder")) { + DBPProject project = getWebProject(projectId, false); + doFileWriteOperation(projectId, project.getMetadataFolder(false), + () -> { + DBPDataSourceRegistry registry = project.getDataSourceRegistry(); + for (String folderPath : folderPaths) { + DBPDataSourceFolder folder = registry.getFolder(folderPath); + if (folder != null) { + log.debug("Deleting data source folder '" + folderPath + "' in project '" + projectId + "'"); + registry.removeFolder(folder, dropContents); + } else { + log.warn("Can not find folder by path [" + folderPath + "] for deletion"); + } + } + registry.checkForErrors(); + return null; + } + ); + } + } + + @Override + public void moveProjectDataSourceFolder( + @NotNull String projectId, + @NotNull String oldPath, + @NotNull String newPath + ) throws DBException { + try (var projectLock = lockController.lockProject(projectId, "createDatasourceFolder")) { + DBPProject project = getWebProject(projectId, false); + log.debug("Moving data source folder from '" + oldPath + "' to '" + newPath + "' in project '" + projectId + "'"); + doFileWriteOperation(projectId, project.getMetadataFolder(false), + () -> { + DBPDataSourceRegistry registry = project.getDataSourceRegistry(); + registry.moveFolder(oldPath, newPath); + registry.checkForErrors(); + return null; + } + ); + } + } + + protected abstract BaseWebProjectImpl getWebProject(String projectId, boolean refresh) throws DBException; + + protected abstract T doFileWriteOperation(String projectId, Path file, RMFileOperation operation) + throws DBException; + + protected abstract T doFileReadOperation(String projectId, Path file, RMFileOperation operation) + throws DBException; + + protected abstract T doProjectOperation(String projectId, RMFileOperation operation) throws DBException; + + protected abstract RMProject makeProjectFromId(String projectId, boolean loadPermissions) throws DBException; + + protected void validateResourcePath(String resourcePath) throws DBException { + var fullPath = Paths.get(resourcePath); + for (Path path : fullPath) { + String fileName = IOUtils.getFileNameWithoutExtension(path); + GeneralUtils.validateResourceName(fileName); + } + } + + protected void createFolder(Path targetPath) throws DBException { + if (!Files.exists(targetPath)) { + try { + Files.createDirectories(targetPath); + } catch (IOException e) { + throw new DBException("Error creating folder '" + targetPath + "'"); + } + } + } + + protected class InternalWebProjectImpl extends BaseWebProjectImpl { + public InternalWebProjectImpl( + @NotNull SessionContextImpl sessionContext, + @NotNull RMProject rmProject, + @NotNull Path projectPath + ) { + super( + BaseLocalResourceController.this.workspace, + BaseLocalResourceController.this, + sessionContext, + rmProject, + projectPath + ); + } + + @NotNull + @Override + protected DBPDataSourceRegistry createDataSourceRegistry() { + return new DataSourceRegistry(this); + } + } + + protected void fireRmResourceAddEvent(@NotNull String projectId, @NotNull String resourcePath) throws DBException { + RMEventManager.fireEvent( + new RMEvent(RMEvent.Action.RESOURCE_ADD, + getProject(projectId, false, false), + resourcePath) + ); + } + + protected void fireRmResourceDeleteEvent(@NotNull String projectId, @NotNull String resourcePath) + throws DBException { + RMEventManager.fireEvent( + new RMEvent(RMEvent.Action.RESOURCE_DELETE, + makeProjectFromId(projectId, false), + resourcePath + ) + ); + } + + protected void fireRmProjectAddEvent(@NotNull RMProject project) { + RMEventManager.fireEvent( + new RMEvent( + RMEvent.Action.RESOURCE_ADD, + 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 21657250bc..b2b33f11a9 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 @@ -29,40 +29,31 @@ import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.DBPDataSourceConfigurationStorage; -import org.jkiss.dbeaver.model.DBPDataSourceContainer; -import org.jkiss.dbeaver.model.DBPDataSourceFolder; -import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; -import org.jkiss.dbeaver.model.app.DBPProject; import org.jkiss.dbeaver.model.app.DBPWorkspace; import org.jkiss.dbeaver.model.auth.SMCredentials; import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; import org.jkiss.dbeaver.model.impl.app.BaseProjectImpl; import org.jkiss.dbeaver.model.impl.auth.SessionContextImpl; import org.jkiss.dbeaver.model.rm.*; -import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor; import org.jkiss.dbeaver.model.security.SMController; import org.jkiss.dbeaver.model.security.SMObjectType; import org.jkiss.dbeaver.model.sql.DBQuotaException; import org.jkiss.dbeaver.model.websocket.event.MessageType; import org.jkiss.dbeaver.model.websocket.event.WSEventType; import org.jkiss.dbeaver.model.websocket.event.WSSessionLogUpdatedEvent; -import org.jkiss.dbeaver.registry.*; +import org.jkiss.dbeaver.registry.ResourceTypeDescriptor; +import org.jkiss.dbeaver.registry.ResourceTypeRegistry; import org.jkiss.dbeaver.runtime.DBWorkbench; -import org.jkiss.dbeaver.utils.GeneralUtils; -import org.jkiss.utils.ArrayUtils; import org.jkiss.utils.CommonUtils; import org.jkiss.utils.IOUtils; import org.jkiss.utils.Pair; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.text.MessageFormat; import java.time.OffsetDateTime; import java.time.ZoneId; import java.util.*; -import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -70,15 +61,10 @@ /** * Resource manager API */ -public class LocalResourceController implements RMController { +public class LocalResourceController extends BaseLocalResourceController { private static final Log log = Log.getLog(LocalResourceController.class); - private static final String FILE_REGEX = "(?U)[\\w.$()@/\\\\ -]+"; - private static final String PROJECT_REGEX = "(?U)[\\w.$()@ -]+"; // slash not allowed in project name - public static final String DEFAULT_CHANGE_ID = "0"; - - private final DBPWorkspace workspace; protected final SMCredentialsProvider credentialsProvider; private final Path rootPath; @@ -86,7 +72,6 @@ public class LocalResourceController implements RMController { private final Path sharedProjectsPath; private final String globalProjectName; private Supplier smControllerSupplier; - protected final RMFileLockController lockController; protected final List fileHandlers; private final Map projectRegistries = new LinkedHashMap<>(); @@ -99,13 +84,12 @@ public LocalResourceController( Path sharedProjectsPath, Supplier smControllerSupplier ) throws DBException { - this.workspace = workspace; + super(workspace, new RMFileLockController(WebAppUtils.getWebApplication())); this.credentialsProvider = credentialsProvider; this.rootPath = rootPath; this.userProjectsPath = userProjectsPath; this.sharedProjectsPath = sharedProjectsPath; this.smControllerSupplier = smControllerSupplier; - this.lockController = new RMFileLockController(WebAppUtils.getWebApplication()); this.globalProjectName = DBWorkbench.getPlatform().getApplication().getDefaultProjectName(); this.fileHandlers = RMFileOperationHandlersRegistry.getInstance().getFileHandlers(); @@ -131,7 +115,7 @@ protected BaseWebProjectImpl getWebProject(String projectId, boolean refresh) th if (project == null || refresh) { SessionContextImpl sessionContext = new SessionContextImpl(null); RMProject rmProject = makeProjectFromId(projectId, false); - project = new InternalWebProjectImpl(sessionContext, rmProject); + project = new InternalWebProjectImpl(sessionContext, rmProject, getProjectPath(projectId)); projectRegistries.put(projectId, project); } return project; @@ -342,182 +326,6 @@ public RMProject getProject(@NotNull String projectId, boolean readResources, bo return project; } - @Override - public Object getProjectProperty(@NotNull String projectId, @NotNull String propName) throws DBException { - var project = getWebProject(projectId, false); - return doFileReadOperation(projectId, project.getMetadataFilePath(), () -> project.getProjectProperty(propName)); - } - - @Override - public void setProjectProperty( - @NotNull String projectId, - @NotNull String propName, - @NotNull Object propValue - ) throws DBException { - BaseWebProjectImpl webProject = getWebProject(projectId, false); - doFileWriteOperation(projectId, webProject.getMetadataFilePath(), - () -> { - log.debug("Updating value for property '" + propName + "' in project '" + projectId + "'"); - webProject.setProjectProperty(propName, propValue); - return null; - } - ); - } - - @Override - public String getProjectsDataSources(@NotNull String projectId, @Nullable String[] dataSourceIds) throws DBException { - DBPProject projectMetadata = getWebProject(projectId, false); - return doFileReadOperation( - projectId, - projectMetadata.getMetadataFolder(false), - () -> { - DBPDataSourceRegistry registry = projectMetadata.getDataSourceRegistry(); - registry.refreshConfig(); - registry.checkForErrors(); - DataSourceConfigurationManagerBuffer buffer = new DataSourceConfigurationManagerBuffer(); - Predicate filter = null; - if (!ArrayUtils.isEmpty(dataSourceIds)) { - filter = ds -> ArrayUtils.contains(dataSourceIds, ds.getId()); - } - ((DataSourcePersistentRegistry) registry).saveConfigurationToManager(new VoidProgressMonitor(), buffer, filter); - registry.checkForErrors(); - - return new String(buffer.getData(), StandardCharsets.UTF_8); - } - ); - } - - @Override - public void createProjectDataSources( - @NotNull String projectId, - @NotNull String configuration, - @Nullable List dataSourceIds - ) throws DBException { - updateProjectDataSources(projectId, configuration, dataSourceIds); - } - - @Override - public boolean updateProjectDataSources( - @NotNull String projectId, - @NotNull String configuration, - @Nullable List dataSourceIds - ) throws DBException { - try (var lock = lockController.lockProject(projectId, "updateProjectDataSources")) { - DBPProject project = getWebProject(projectId, false); - return doFileWriteOperation(projectId, project.getMetadataFolder(false), - () -> { - DBPDataSourceRegistry registry = project.getDataSourceRegistry(); - DBPDataSourceConfigurationStorage storage = new DataSourceMemoryStorage(configuration.getBytes(StandardCharsets.UTF_8)); - DataSourceConfigurationManager manager = new DataSourceConfigurationManagerBuffer(); - var configChanged = ((DataSourcePersistentRegistry) registry).loadDataSources( - List.of(storage), - manager, - dataSourceIds, - true, - false - ); - registry.checkForErrors(); - log.debug("Save data sources configuration in project '" + projectId + "'"); - ((DataSourcePersistentRegistry) registry).saveDataSources(); - registry.checkForErrors(); - return configChanged; - } - ); - } - } - - @Override - public void deleteProjectDataSources(@NotNull String projectId, - @NotNull String[] dataSourceIds) throws DBException { - try (var projectLock = lockController.lockProject(projectId, "deleteDatasources")) { - DBPProject project = getWebProject(projectId, false); - doFileWriteOperation(projectId, project.getMetadataFolder(false), () -> { - DBPDataSourceRegistry registry = project.getDataSourceRegistry(); - for (String dataSourceId : dataSourceIds) { - DBPDataSourceContainer dataSource = registry.getDataSource(dataSourceId); - - if (dataSource != null) { - log.debug("Deleting data source '" + dataSourceId + "' in project '" + projectId + "'"); - registry.removeDataSource(dataSource); - } else { - log.warn("Could not find datasource " + dataSourceId + " for deletion"); - } - } - registry.checkForErrors(); - return null; - }); - } - } - - @Override - public void createProjectDataSourceFolder(@NotNull String projectId, - @NotNull String folderPath) throws DBException { - try (var projectLock = lockController.lockProject(projectId, "createDatasourceFolder")) { - DBPProject project = getWebProject(projectId, false); - log.debug("Creating data source folder '" + folderPath + "' in project '" + projectId + "'"); - doFileWriteOperation(projectId, project.getMetadataFolder(false), - () -> { - DBPDataSourceRegistry registry = project.getDataSourceRegistry(); - var result = Path.of(folderPath); - var newName = result.getFileName().toString(); - GeneralUtils.validateResourceName(newName); - var parent = result.getParent(); - var parentFolder = parent == null ? null : registry.getFolder(parent.toString().replace("\\", "/")); - DBPDataSourceFolder newFolder = registry.addFolder(parentFolder, newName); - registry.checkForErrors(); - return null; - } - ); - } - } - - @Override - public void deleteProjectDataSourceFolders( - @NotNull String projectId, - @NotNull String[] folderPaths, - boolean dropContents - ) throws DBException { - try (var projectLock = lockController.lockProject(projectId, "createDatasourceFolder")) { - DBPProject project = getWebProject(projectId, false); - doFileWriteOperation(projectId, project.getMetadataFolder(false), - () -> { - DBPDataSourceRegistry registry = project.getDataSourceRegistry(); - for (String folderPath : folderPaths) { - DBPDataSourceFolder folder = registry.getFolder(folderPath); - if (folder != null) { - log.debug("Deleting data source folder '" + folderPath + "' in project '" + projectId + "'"); - registry.removeFolder(folder, dropContents); - } else { - log.warn("Can not find folder by path [" + folderPath + "] for deletion"); - } - } - registry.checkForErrors(); - return null; - } - ); - } - } - - @Override - public void moveProjectDataSourceFolder( - @NotNull String projectId, - @NotNull String oldPath, - @NotNull String newPath - ) throws DBException { - try (var projectLock = lockController.lockProject(projectId, "createDatasourceFolder")) { - DBPProject project = getWebProject(projectId, false); - log.debug("Moving data source folder from '" + oldPath + "' to '" + newPath + "' in project '" + projectId + "'"); - doFileWriteOperation(projectId, project.getMetadataFolder(false), - () -> { - DBPDataSourceRegistry registry = project.getDataSourceRegistry(); - registry.moveFolder(oldPath, newPath); - registry.checkForErrors(); - return null; - } - ); - } - } - @NotNull @Override public RMResource[] listResources( @@ -625,7 +433,7 @@ public String moveResource( throw new DBException("Resource '" + oldTargetPath + "' doesn't exists"); } Path newTargetPath = getTargetPath(projectId, normalizedNewResourcePath); - validateResourcePath(newTargetPath.toString()); + validateResourcePath(rootPath.relativize(newTargetPath).toString()); if (Files.exists(newTargetPath)) { throw new DBException("Resource with name %s already exists".formatted(newTargetPath.getFileName())); } @@ -804,15 +612,6 @@ public String setResourceContents( return DEFAULT_CHANGE_ID; } - protected void createFolder(Path targetPath) throws DBException { - if (!Files.exists(targetPath)) { - try { - Files.createDirectories(targetPath); - } catch (IOException e) { - throw new DBException("Error creating folder '" + targetPath + "'"); - } - } - } @NotNull @Override @@ -857,14 +656,6 @@ public String setResourceProperties( } } - private void validateResourcePath(String resourcePath) throws DBException { - var fullPath = Paths.get(resourcePath); - for (Path path : fullPath) { - String fileName = IOUtils.getFileNameWithoutExtension(path); - GeneralUtils.validateResourceName(fileName); - } - } - @NotNull private Path getTargetPath(@NotNull String projectId, @NotNull String resourcePath) throws DBException { Path projectPath = getProjectPath(projectId); @@ -881,7 +672,7 @@ private Path getTargetPath(@NotNull String projectId, @NotNull String resourcePa if (!targetPath.startsWith(projectPath)) { throw new DBException("Invalid resource path"); } - return WebAppUtils.getWebApplication().getHomeDirectory().relativize(targetPath); + return targetPath; } catch (InvalidPathException e) { throw new DBException("Resource path contains invalid characters"); } @@ -894,7 +685,7 @@ private String makeProjectIdFromPath(Path path, RMProjectType type) { } @Nullable - private RMProject makeProjectFromId(String projectId, boolean loadPermissions) throws DBException { + protected RMProject makeProjectFromId(String projectId, boolean loadPermissions) throws DBException { var projectName = parseProjectName(projectId); var projectPath = getProjectPath(projectId); if (!Files.exists(projectPath)) { @@ -1138,32 +929,6 @@ private String getProjectRelativePath(@NotNull String projectId, @NotNull Path p return getProjectPath(projectId).toAbsolutePath().relativize(path).toString().replace('\\', IPath.SEPARATOR); } - private void fireRmResourceAddEvent(@NotNull String projectId, @NotNull String resourcePath) throws DBException { - RMEventManager.fireEvent( - new RMEvent(RMEvent.Action.RESOURCE_ADD, - getProject(projectId, false, false), - resourcePath) - ); - } - - private void fireRmResourceDeleteEvent(@NotNull String projectId, @NotNull String resourcePath) throws DBException { - RMEventManager.fireEvent( - new RMEvent(RMEvent.Action.RESOURCE_DELETE, - makeProjectFromId(projectId, false), - resourcePath - ) - ); - } - - private void fireRmProjectAddEvent(@NotNull RMProject project) { - RMEventManager.fireEvent( - new RMEvent( - RMEvent.Action.RESOURCE_ADD, - project - ) - ); - } - protected void handleProjectOpened(String projectId) throws DBException { createResourceTypeFolders(getProjectPath(projectId)); } @@ -1284,21 +1049,4 @@ public static boolean isProjectOwner(String projectId, String userId) { rmProjectName.name.equals(userId); } - - private class InternalWebProjectImpl extends BaseWebProjectImpl { - public InternalWebProjectImpl(SessionContextImpl sessionContext, RMProject rmProject) { - super( - LocalResourceController.this.workspace, - LocalResourceController.this, - sessionContext, - rmProject); - } - - @NotNull - @Override - protected DBPDataSourceRegistry createDataSourceRegistry() { - return new DataSourceRegistry(this); - } - } - } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMFileLockController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMFileLockController.java index bf966ea748..b87526cd92 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMFileLockController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMFileLockController.java @@ -23,6 +23,7 @@ import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.app.DBPWorkspace; +import org.jkiss.utils.IOUtils; import java.io.IOException; import java.io.Reader; @@ -73,18 +74,23 @@ public RMFileLockController(WebApplication application, int maxLockTime) throws * @return - lock */ @NotNull - public RMLock lockProject(@NotNull String projectId,@NotNull String operationName) throws DBException { + public RMLock lockProject(@NotNull String projectId, @NotNull String operationName) throws DBException { synchronized (RMFileLockController.class) { try { - createLockFolderIfNeeded(); - createProjectFolder(projectId); - Path projectLockFile = getProjectLockFilePath(projectId); - RMLockInfo lockInfo = new RMLockInfo.Builder(projectId, UUID.randomUUID().toString()) .setApplicationId(applicationId) .setOperationName(operationName) .setOperationStartTime(System.currentTimeMillis()) .build(); + Path projectLockFile = getProjectLockFilePath(projectId); + + if (!IOUtils.isFileFromDefaultFS(lockFolderPath)) { + // fake lock for external file system? + return new RMLock(projectLockFile); + } + createLockFolderIfNeeded(); + createProjectFolder(projectId); + createLockFile(projectLockFile, lockInfo); return new RMLock(projectLockFile); } catch (Exception e) { diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java index 9cad92722f..43b69e137b 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java @@ -63,11 +63,16 @@ public BaseWebSession(@NotNull String id, @NotNull WebApplication application) t this.application = application; this.createTime = System.currentTimeMillis(); this.lastAccessTime = this.createTime; - this.workspace = new WebSessionWorkspace(this); + this.workspace = createWebWorkspace(); this.workspace.getAuthContext().addSession(this); this.userContext = createUserContext(); } + @NotNull + protected WebSessionWorkspace createWebWorkspace() { + return new WebSessionWorkspace(this); + } + protected WebUserContext createUserContext() throws DBException { return new WebUserContext(this.application, this.workspace); } 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 e78b45d9b2..ce10a80553 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 @@ -69,6 +69,7 @@ import org.jkiss.utils.CommonUtils; import java.lang.reflect.InvocationTargetException; +import java.nio.file.Path; import java.time.Instant; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; @@ -297,7 +298,7 @@ private void loadProjects() { for (RMProject project : rmProjects) { createWebProject(project); } - if (user == null) { + if (user == null && application.getAppConfiguration().isAnonymousAccessEnabled()) { WebProjectImpl anonymousProject = createWebProject(RMUtils.createAnonymousProject()); anonymousProject.setInMemory(true); } @@ -310,12 +311,12 @@ private void loadProjects() { } } - private WebSessionProjectImpl createWebProject(RMProject project) { + private WebSessionProjectImpl createWebProject(RMProject project) throws DBException { WebSessionProjectImpl sessionProject; if (project.isGlobal()) { sessionProject = createGlobalProject(project); } else { - sessionProject = new WebSessionProjectImpl(this, project); + sessionProject = new WebSessionProjectImpl(this, project, getProjectPath(project)); } // do not load data sources for anonymous project if (project.getType() == RMProjectType.USER && userContext.getUser() == null) { @@ -328,6 +329,11 @@ private WebSessionProjectImpl createWebProject(RMProject project) { return sessionProject; } + @NotNull + protected Path getProjectPath(@NotNull RMProject project) throws DBException { + return RMUtils.getProjectPath(project); + } + protected WebSessionProjectImpl createGlobalProject(RMProject project) { globalProject = new WebSessionGlobalProjectImpl(this, project); globalProject.refreshAccessibleConnectionIds(); diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/BaseGQLPlatform.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/BaseGQLPlatform.java index ca5d26fd6a..a2ae8040a2 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/BaseGQLPlatform.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/BaseGQLPlatform.java @@ -17,6 +17,7 @@ package io.cloudbeaver.server; import io.cloudbeaver.DBWConstants; +import io.cloudbeaver.model.app.WebApplication; import org.eclipse.core.runtime.Plugin; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; @@ -33,6 +34,8 @@ import org.jkiss.dbeaver.runtime.qm.QMLogFileWriter; import org.jkiss.dbeaver.runtime.qm.QMRegistryImpl; import org.jkiss.dbeaver.utils.ContentUtils; +import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.StandardConstants; import java.io.IOException; import java.nio.file.Files; @@ -40,7 +43,7 @@ public abstract class BaseGQLPlatform extends BasePlatformImpl { private static final Log log = Log.getLog(BaseGQLPlatform.class); - public static final String WORK_DATA_FOLDER_NAME = ".work-data"; + public static final String BASE_TEMP_DIR = "dbeaver"; private Path tempFolder; @@ -55,7 +58,7 @@ protected synchronized void initialize() { SecurityProviderUtils.registerSecurityProvider(); // Register properties adapter - this.workspace = new WebGlobalWorkspace(this); + this.workspace = new WebGlobalWorkspace(this, (WebApplication) getApplication()); this.workspace.initializeProjects(); QMUtils.initApplication(this); @@ -92,16 +95,12 @@ public DBPWorkspace getWorkspace() { @NotNull public Path getTempFolder(@NotNull DBRProgressMonitor monitor, @NotNull String name) { + if (tempFolder == null) { - // Make temp folder - monitor.subTask("Create temp folder"); - tempFolder = workspace.getAbsolutePath().resolve(DBWConstants.WORK_DATA_FOLDER_NAME); - } - if (!Files.exists(tempFolder)) { - try { - Files.createDirectories(tempFolder); - } catch (IOException e) { - log.error("Can't create temp directory " + tempFolder, e); + synchronized (this) { + if (tempFolder == null) { + initTempFolder(monitor); + } } } Path folder = tempFolder.resolve(name); @@ -115,6 +114,21 @@ public Path getTempFolder(@NotNull DBRProgressMonitor monitor, @NotNull String n return folder; } + private void initTempFolder(@NotNull DBRProgressMonitor monitor) { + // Make temp folder + monitor.subTask("Create temp folder"); + String sysTempFolder = System.getProperty(StandardConstants.ENV_TMP_DIR); + if (CommonUtils.isNotEmpty(sysTempFolder)) { + tempFolder = Path.of(sysTempFolder).resolve(BASE_TEMP_DIR).resolve(DBWConstants.WORK_DATA_FOLDER_NAME); + } else { + //we do not use workspace because it can be in external file system + tempFolder = getApplication().getHomeDirectory().resolve(DBWConstants.WORK_DATA_FOLDER_NAME); + } + } + + @NotNull + public abstract WebApplication getApplication(); + @Override public synchronized void dispose() { super.dispose(); diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalWorkspace.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalWorkspace.java index 997c0dcded..d44262923c 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalWorkspace.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalWorkspace.java @@ -19,7 +19,6 @@ import io.cloudbeaver.WebProjectImpl; import io.cloudbeaver.model.app.WebApplication; import io.cloudbeaver.utils.WebAppUtils; -import org.eclipse.core.runtime.Platform; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.Log; @@ -30,8 +29,6 @@ import org.jkiss.utils.CommonUtils; import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; @@ -49,21 +46,14 @@ public class WebGlobalWorkspace extends BaseWorkspaceImpl { protected final Map projects = new LinkedHashMap<>(); private WebGlobalProject globalProject; - public WebGlobalWorkspace(DBPPlatform platform) { - super(platform, - platform.getApplication().isMultiuser() - ? Path.of(getWorkspaceURI()) - : ((WebApplication) platform.getApplication()).getWorkspaceDirectory()); - } + private final WebApplication application; - @NotNull - private static URI getWorkspaceURI() { - String workspacePath = Platform.getInstanceLocation().getURL().toString(); - try { - return new URI(workspacePath); - } catch (URISyntaxException e) { - throw new IllegalStateException("Workspace path is invalid: " + workspacePath, e); - } + public WebGlobalWorkspace( + @NotNull DBPPlatform platform, + @NotNull WebApplication application + ) { + super(platform, application.getWorkspaceDirectory()); + this.application = application; } @Override @@ -109,7 +99,7 @@ public List getProjects() { @Nullable @Override public BaseProjectImpl getProject(@NotNull String projectName) { - if (globalProject.getId().equals(projectName)) { + if (globalProject != null && globalProject.getId().equals(projectName)) { return globalProject; } return null; diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebPlatformActivator.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebPlatformActivator.java index 94652dd478..d13eb03bcf 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebPlatformActivator.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebPlatformActivator.java @@ -23,7 +23,6 @@ import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; -import java.io.File; import java.io.PrintStream; /** @@ -33,7 +32,6 @@ public class WebPlatformActivator extends Plugin { // The shared instance private static WebPlatformActivator instance; - private static File configDir; private PrintStream debugWriter; private DBPPreferenceStore preferences; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPreferenceStore.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebServerPreferenceStore.java similarity index 93% rename from server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPreferenceStore.java rename to server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebServerPreferenceStore.java index a40bf64584..681262bf36 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPreferenceStore.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebServerPreferenceStore.java @@ -16,6 +16,7 @@ */ package io.cloudbeaver.server; +import io.cloudbeaver.utils.WebAppUtils; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.model.impl.preferences.AbstractPreferenceStore; import org.jkiss.dbeaver.model.preferences.DBPPreferenceStore; @@ -23,16 +24,12 @@ import java.io.IOException; import java.util.Map; -public class CBPreferenceStore extends AbstractPreferenceStore { - @NotNull - private final CBPlatform cbPlatform; +public class WebServerPreferenceStore extends AbstractPreferenceStore { private final DBPPreferenceStore parentStore; - public CBPreferenceStore( - @NotNull CBPlatform cbPlatform, + public WebServerPreferenceStore( @NotNull DBPPreferenceStore parentStore ) { - this.cbPlatform = cbPlatform; this.parentStore = parentStore; } @@ -188,7 +185,7 @@ public void save() throws IOException { } private Map productConf() { - var app = cbPlatform.getApplication(); - return app.getProductConfiguration(); + var app = WebAppUtils.getWebApplication(); + return app.getServerConfiguration().getProductSettings(); } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java index e1ddccf294..46ff6fa6bb 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java @@ -19,6 +19,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.InstanceCreator; +import com.google.gson.Strictness; import io.cloudbeaver.model.WebConnectionConfig; import io.cloudbeaver.model.WebNetworkHandlerConfigInput; import io.cloudbeaver.model.WebPropertyInfo; @@ -294,7 +295,7 @@ public static void saveAuthProperties( // Make new Gson parser with type adapters to deserialize into existing credentials InstanceCreator credTypeAdapter = type -> credentials; Gson credGson = new GsonBuilder() - .setLenient() + .setStrictness(Strictness.LENIENT) .registerTypeAdapter(credentials.getClass(), credTypeAdapter) .create(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java index 0bf84aae33..18fde20361 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java @@ -59,13 +59,13 @@ import org.jkiss.utils.StandardConstants; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.URL; import java.net.UnknownHostException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -109,6 +109,8 @@ public static CBApplication getInstance() { private final Map initActions = new ConcurrentHashMap<>(); + private CBJettyServer jettyServer; + public CBApplication() { this.homeDirectory = new File(initHomeFolder()); } @@ -201,6 +203,7 @@ protected void startServer() { if (!loadServerConfiguration()) { return; } + if (CommonUtils.isEmpty(this.getAppConfiguration().getDefaultUserTeam())) { throw new DBException("Default user team must be specified"); } @@ -208,6 +211,7 @@ protected void startServer() { log.error(e); return; } + refreshDisabledDriversConfig(); configurationMode = CommonUtils.isEmpty(getServerConfiguration().getServerName()); @@ -303,7 +307,7 @@ protected void startServer() { if (configurationMode) { // Try to configure automatically - performAutoConfiguration(getMainConfigurationFilePath().toFile().getParentFile()); + performAutoConfiguration(getMainConfigurationFilePath().getParent()); } else if (!isMultiNode()) { var appConfiguration = getServerConfigurationController().getAppConfiguration(); if (appConfiguration.isGrantConnectionsAccessToAnonymousTeam()) { @@ -331,7 +335,7 @@ protected void initializeAdditionalConfiguration() { * * @param configPath */ - protected void performAutoConfiguration(File configPath) { + protected void performAutoConfiguration(Path configPath) { String autoServerName = System.getenv(CBConstants.VAR_AUTO_CB_SERVER_NAME); String autoServerURL = System.getenv(CBConstants.VAR_AUTO_CB_SERVER_URL); String autoAdminName = System.getenv(CBConstants.VAR_AUTO_CB_ADMIN_NAME); @@ -340,11 +344,11 @@ protected void performAutoConfiguration(File configPath) { if (CommonUtils.isEmpty(autoServerName) || CommonUtils.isEmpty(autoAdminName) || CommonUtils.isEmpty( autoAdminPassword)) { // Try to load from auto config file - if (configPath.exists()) { - File autoConfigFile = new File(configPath, CBConstants.AUTO_CONFIG_FILE_NAME); - if (autoConfigFile.exists()) { + if (Files.exists(configPath)) { + Path autoConfigFile = configPath.resolve(CBConstants.AUTO_CONFIG_FILE_NAME); + if (Files.exists(autoConfigFile)) { Properties autoProps = new Properties(); - try (InputStream is = new FileInputStream(autoConfigFile)) { + try (InputStream is = Files.newInputStream(autoConfigFile)) { autoProps.load(is); autoServerName = autoProps.getProperty(CBConstants.VAR_AUTO_CB_SERVER_NAME); @@ -352,7 +356,7 @@ protected void performAutoConfiguration(File configPath) { autoAdminName = autoProps.getProperty(CBConstants.VAR_AUTO_CB_ADMIN_NAME); autoAdminPassword = autoProps.getProperty(CBConstants.VAR_AUTO_CB_ADMIN_PASSWORD); } catch (IOException e) { - log.error("Error loading auto configuration file '" + autoConfigFile.getAbsolutePath() + "'", + log.error("Error loading auto configuration file '" + autoConfigFile + "'", e); } } @@ -439,11 +443,6 @@ public Path getDataDirectory(boolean create) { return dataDir.toPath(); } - @Override - public Path getWorkspaceDirectory() { - return Path.of(getServerConfiguration().getWorkspaceLocation()); - } - private void initializeSecurityController() throws DBException { securityController = createGlobalSecurityController(); } @@ -468,7 +467,8 @@ private void runWebServer() { getServerPort(), CommonUtils.isEmpty(getServerHost()) ? "all interfaces" : getServerHost()) ); - new CBJettyServer(this).runServer(); + this.jettyServer = new CBJettyServer(this); + this.jettyServer.runServer(); } @@ -568,6 +568,9 @@ public synchronized void reloadConfiguration(@Nullable SMCredentialsProvider cre sendConfigChangedEvent(credentialsProvider); eventController.setForceSkipEvents(isConfigurationMode()); + if (this.jettyServer != null) { + this.jettyServer.refreshJettyConfig(); + } } protected abstract void finishSecurityServiceConfiguration( diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java index 1c4e0fadd5..0cc1833109 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java @@ -59,7 +59,7 @@ public class CBPlatform extends BaseGQLPlatform { @Nullable private static GQLApplicationAdapter application = null; - private CBPreferenceStore preferenceStore; + private WebServerPreferenceStore preferenceStore; protected final List applicableDrivers = new ArrayList<>(); public static CBPlatform getInstance() { @@ -77,7 +77,7 @@ public static void setApplication(@NotNull GQLApplicationAdapter application) { protected synchronized void initialize() { long startTime = System.currentTimeMillis(); log.info("Initialize web platform...: "); - this.preferenceStore = new CBPreferenceStore(this, WebPlatformActivator.getInstance().getPreferences()); + this.preferenceStore = new WebServerPreferenceStore(WebPlatformActivator.getInstance().getPreferences()); super.initialize(); refreshApplicableDrivers(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationController.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationController.java index 7acde3722b..637ea382d3 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationController.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationController.java @@ -16,9 +16,7 @@ */ package io.cloudbeaver.server; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.InstanceCreator; +import com.google.gson.*; import io.cloudbeaver.model.app.BaseServerConfigurationController; import io.cloudbeaver.model.app.BaseWebApplication; import io.cloudbeaver.model.config.CBAppConfig; @@ -42,6 +40,7 @@ import org.jkiss.dbeaver.utils.PrefUtils; import org.jkiss.dbeaver.utils.SystemVariablesResolver; import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.IOUtils; import java.io.*; import java.net.InetAddress; @@ -68,6 +67,7 @@ public abstract class CBServerConfigurationController private final Map originalConfigurationProperties = new LinkedHashMap<>(); protected CBServerConfigurationController(@NotNull T serverConfiguration, @NotNull Path homeDirectory) { + super(homeDirectory); this.serverConfiguration = serverConfiguration; this.homeDirectory = homeDirectory; } @@ -91,17 +91,25 @@ public void loadServerConfiguration(Path configPath) throws DBException { loadConfiguration(configPath); } + initWorkspacePath(); + // Try to load configuration from runtime app config file Path runtimeConfigPath = getRuntimeAppConfigPath(); if (Files.exists(runtimeConfigPath)) { log.debug("Runtime configuration [" + runtimeConfigPath.toAbsolutePath() + "]"); loadConfiguration(runtimeConfigPath); } - // Set default preferences PrefUtils.setDefaultPreferenceValue(DBWorkbench.getPlatform().getPreferenceStore(), ModelPreferences.UI_DRIVERS_HOME, getServerConfiguration().getDriversLocation()); + validateFinalServerConfiguration(); + } + + @NotNull + @Override + protected String getWorkspaceLocation() { + return getServerConfiguration().getWorkspaceLocation(); } public void loadConfiguration(Path configPath) throws DBException { @@ -146,7 +154,7 @@ protected void parseConfiguration(Map configProps) throws DBExce ); // App config Map appConfig = JSONUtils.getObject(configProps, "app"); - validateConfiguration(appConfig); + preValidateAppConfiguration(appConfig); gson.fromJson(gson.toJson(appConfig), CBAppConfig.class); readProductConfiguration(serverConfig, gson); } @@ -169,7 +177,6 @@ public T parseServerConfiguration() { config.setContentRoot(WebAppUtils.getRelativePath(config.getContentRoot(), homeDirectory)); config.setRootURI(readRootUri(config.getRootURI())); config.setDriversLocation(WebAppUtils.getRelativePath(config.getDriversLocation(), homeDirectory)); - config.setWorkspaceLocation(WebAppUtils.getRelativePath(config.getWorkspaceLocation(), homeDirectory)); String staticContentsFile = config.getStaticContent(); if (!CommonUtils.isEmpty(staticContentsFile)) { @@ -182,10 +189,11 @@ public T parseServerConfiguration() { return config; } - protected void validateConfiguration(Map appConfig) throws DBException { + protected void preValidateAppConfiguration(Map appConfig) throws DBException { } + private void readExternalProperties(Map serverConfig) { String externalPropertiesFile = JSONUtils.getString(serverConfig, CBConstants.PARAM_EXTERNAL_PROPERTIES); if (!CommonUtils.isEmpty(externalPropertiesFile)) { @@ -248,19 +256,21 @@ protected void readProductConfiguration(Map serverConfig, Gson g } } - // Add product config from runtime - File rtConfig = getRuntimeProductConfigFilePath().toFile(); - if (rtConfig.exists()) { - log.debug("Load product runtime configuration from '" + rtConfig.getAbsolutePath() + "'"); - try (Reader reader = new InputStreamReader(new FileInputStream(rtConfig), StandardCharsets.UTF_8)) { - var runtimeProductSettings = JSONUtils.parseMap(gson, reader); - var productSettings = serverConfiguration.getProductSettings(); - runtimeProductSettings.putAll(productSettings); - Map flattenConfig = WebAppUtils.flattenMap(runtimeProductSettings); - productSettings.clear(); - productSettings.putAll(flattenConfig); - } catch (Exception e) { - throw new DBException("Error reading product runtime configuration", e); + if (workspacePath != null && IOUtils.isFileFromDefaultFS(getWorkspacePath())) { + // Add product config from runtime + Path rtConfig = getRuntimeProductConfigFilePath(); + if (Files.exists(rtConfig)) { + log.debug("Load product runtime configuration from '" + rtConfig + "'"); + try (Reader reader = new InputStreamReader(Files.newInputStream(rtConfig), StandardCharsets.UTF_8)) { + var runtimeProductSettings = JSONUtils.parseMap(gson, reader); + var productSettings = serverConfiguration.getProductSettings(); + runtimeProductSettings.putAll(productSettings); + Map flattenConfig = WebAppUtils.flattenMap(runtimeProductSettings); + productSettings.clear(); + productSettings.putAll(flattenConfig); + } catch (Exception e) { + throw new DBException("Error reading product runtime configuration", e); + } } } } @@ -307,13 +317,14 @@ protected Map readConfiguration(Path configPath) throws DBExcept } public Map readConfigurationFile(Path path) throws DBException { - try (Reader reader = new InputStreamReader(new FileInputStream(path.toFile()), StandardCharsets.UTF_8)) { + try (Reader reader = new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8)) { return JSONUtils.parseMap(getGson(), reader); } catch (Exception e) { throw new DBException("Error parsing server configuration", e); } } + @NotNull protected GsonBuilder getGsonBuilder() { // Stupid way to populate existing objects but ok google (https://github.com/google/gson/issues/431) InstanceCreator appConfigCreator = type -> appConfiguration; @@ -324,7 +335,8 @@ protected GsonBuilder getGsonBuilder() { InstanceCreator smPasswordPoliceConfigCreator = type -> securityManagerConfiguration.getPasswordPolicyConfiguration(); return new GsonBuilder() - .setLenient() + .setStrictness(Strictness.LENIENT) + .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) .registerTypeAdapter(getServerConfiguration().getClass(), serverConfigCreator) .registerTypeAdapter(CBAppConfig.class, appConfigCreator) .registerTypeAdapter(DataSourceNavigatorSettings.class, navSettingsCreator) @@ -358,10 +370,9 @@ private synchronized void writeRuntimeConfig(Path runtimeConfigPath, Map productConfiguration) throws DBException { @@ -633,4 +646,9 @@ private String readRootUri(String uri) { public Map getOriginalConfigurationProperties() { return originalConfigurationProperties; } + + @Override + public void validateFinalServerConfiguration() throws DBException { + + } } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationControllerEmbedded.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationControllerEmbedded.java index 7031b941a6..87d28cd7d1 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationControllerEmbedded.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationControllerEmbedded.java @@ -92,6 +92,7 @@ private void savePasswordPolicyConfig(Map originServerConfig, Ma } } + @NotNull @Override protected GsonBuilder getGsonBuilder() { GsonBuilder gsonBuilder = super.getGsonBuilder(); @@ -100,6 +101,4 @@ protected GsonBuilder getGsonBuilder() { return gsonBuilder .registerTypeAdapter(WebDatabaseConfig.class, dbConfigCreator); } - - } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/AbstractActionServletHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/AbstractActionServletHandler.java index f6c68805f8..bcf97f7aaf 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/AbstractActionServletHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/AbstractActionServletHandler.java @@ -18,6 +18,7 @@ import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.service.DBWServletHandler; +import io.cloudbeaver.utils.WebAppUtils; import jakarta.servlet.Servlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -43,7 +44,7 @@ protected void createActionFromParams(WebSession session, HttpServletRequest req action.saveInSession(session); // Redirect to home - response.sendRedirect("/"); + response.sendRedirect(WebAppUtils.getWebApplication().getServerConfiguration().getRootURI()); } protected abstract String getActionConsole(); 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 ebc1dfd6e1..7f4ebeb740 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 @@ -19,100 +19,162 @@ import io.cloudbeaver.WebSessionGlobalProjectImpl; import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.server.CBPlatform; +import io.cloudbeaver.service.security.SMUtils; 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.SMObjectType; +import org.jkiss.dbeaver.model.security.SMAdminController; +import org.jkiss.dbeaver.model.security.SMObjectPermissionsGrant; 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.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; public class WSObjectPermissionUpdatedEventHandler extends WSDefaultEventHandler { private static final Log log = Log.getLog(WSObjectPermissionUpdatedEventHandler.class); @Override - protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @NotNull WSObjectPermissionEvent event) { - try { + public void handleEvent(@NotNull WSObjectPermissionEvent event) { + String objectId = event.getObjectId(); + Consumer runnable = switch (event.getSmObjectType()) { + case project: + yield getUpdateUserProjectsInfoConsumer(event, objectId); + case datasource: + try { + SMAdminController smController = CBApplication.getInstance().getSecurityController(); + Set dataSourcePermissions = smController.getObjectPermissionGrants(event.getObjectId(), event.getSmObjectType()) + .stream() + .map(SMObjectPermissionsGrant::getSubjectId).collect(Collectors.toSet()); + yield getUpdateUserDataSourcesInfoConsumer(event, objectId, dataSourcePermissions); + } catch (DBException e) { + log.error("Error getting permissions for data source " + objectId, e); + yield null; + } + }; + if (runnable == null) { + return; + } + log.debug(event.getTopicId() + " event handled"); + Collection allSessions = CBPlatform.getInstance().getSessionManager().getAllActiveSessions(); + for (var activeUserSession : allSessions) { + if (!isAcceptableInSession(activeUserSession, event)) { + log.debug("Cannot handle %s event '%s' in session %s".formatted( + event.getTopicId(), + event.getId(), + activeUserSession.getSessionId() + )); + continue; + } + log.debug("%s event '%s' handled".formatted(event.getTopicId(), event.getId())); + runnable.accept(activeUserSession); + } + } + + @NotNull + private Consumer getUpdateUserDataSourcesInfoConsumer( + @NotNull WSObjectPermissionEvent event, + @NotNull String dataSourceId, + @NotNull Set dataSourcePermissions + ) { + return (activeUserSession) -> { // we have accessible data sources only in web session - if (event.getSmObjectType() == SMObjectType.datasource && !(activeUserSession instanceof WebSession)) { + // admins already have access for all shared connections + if (!(activeUserSession instanceof WebSession webSession) || SMUtils.isAdmin(webSession)) { return; } - var objectId = event.getObjectId(); - - boolean isAccessibleNow; - switch (event.getSmObjectType()) { - case project: - 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( - event.getSessionId(), - event.getUserId(), - objectId - ) - ); - } else if (WSEventType.OBJECT_PERMISSIONS_DELETED.getEventId().equals(event.getId())) { - activeUserSession.removeSessionProject(objectId); - activeUserSession.addSessionEvent( - WSProjectUpdateEvent.delete( - event.getSessionId(), - event.getUserId(), - objectId - ) - ); - } - break; - case datasource: - var webSession = (WebSession) activeUserSession; - var dataSources = List.of(objectId); + if (!isAcceptableInSession(webSession, event)) { + return; + } + var user = activeUserSession.getUserContext().getUser(); + var userSubjects = new HashSet<>(Set.of(user.getTeams())); + userSubjects.add(user.getUserId()); + boolean shouldBeAccessible = dataSourcePermissions.stream().anyMatch(userSubjects::contains); + List dataSources = List.of(dataSourceId); + WebSessionGlobalProjectImpl project = webSession.getGlobalProject(); + if (project == null) { + log.error("Project " + WebAppUtils.getGlobalProjectId() + + " is not found in session " + activeUserSession.getSessionId()); + return; + } + boolean isAccessibleNow = project.findWebConnectionInfo(dataSourceId) != null; + if (WSEventType.OBJECT_PERMISSIONS_UPDATED.getEventId().equals(event.getId())) { + if (isAccessibleNow || !shouldBeAccessible) { + return; + } + project.addAccessibleConnectionToCache(dataSourceId); + webSession.addSessionEvent( + WSDataSourceEvent.create( + event.getSessionId(), + event.getUserId(), + project.getId(), + dataSources, + WSDataSourceProperty.CONFIGURATION + ) + ); + } else if (WSEventType.OBJECT_PERMISSIONS_DELETED.getEventId().equals(event.getId())) { + if (!isAccessibleNow || shouldBeAccessible) { + return; + } + project.removeAccessibleConnectionFromCache(dataSourceId); + webSession.addSessionEvent( + WSDataSourceEvent.delete( + event.getSessionId(), + event.getUserId(), + project.getId(), + dataSources, + WSDataSourceProperty.CONFIGURATION + ) + ); + } + }; + } - WebSessionGlobalProjectImpl project = webSession.getGlobalProject(); - if (project == null) { - log.error("Project " + WebAppUtils.getGlobalProjectId() + - " is not found in session " + activeUserSession.getSessionId()); + @NotNull + private Consumer getUpdateUserProjectsInfoConsumer( + @NotNull WSObjectPermissionEvent event, + @NotNull String projectId + ) { + return (activeUserSession) -> { + try { + if (WSEventType.OBJECT_PERMISSIONS_UPDATED.getEventId().equals(event.getId())) { + var accessibleProjectIds = activeUserSession.getUserContext().getAccessibleProjectIds(); + if (accessibleProjectIds.contains(event.getObjectId())) { return; } - if (WSEventType.OBJECT_PERMISSIONS_UPDATED.getEventId().equals(event.getId())) { - isAccessibleNow = project.findWebConnectionInfo(objectId) != null; - if (isAccessibleNow) { - return; - } - project.addAccessibleConnectionToCache(objectId); - webSession.addSessionEvent( - WSDataSourceEvent.create( - event.getSessionId(), - event.getUserId(), - project.getId(), - dataSources, - WSDataSourceProperty.CONFIGURATION - ) - ); - } else if (WSEventType.OBJECT_PERMISSIONS_DELETED.getEventId().equals(event.getId())) { - project.removeAccessibleConnectionFromCache(objectId); - webSession.addSessionEvent( - WSDataSourceEvent.delete( - event.getSessionId(), - event.getUserId(), - project.getId(), - dataSources, - WSDataSourceProperty.CONFIGURATION - ) - ); - } + activeUserSession.addSessionProject(projectId); + activeUserSession.addSessionEvent( + WSProjectUpdateEvent.create( + event.getSessionId(), + event.getUserId(), + projectId + ) + ); + } else if (WSEventType.OBJECT_PERMISSIONS_DELETED.getEventId().equals(event.getId())) { + activeUserSession.removeSessionProject(projectId); + activeUserSession.addSessionEvent( + WSProjectUpdateEvent.delete( + event.getSessionId(), + event.getUserId(), + projectId + ) + ); + } + } catch (DBException e) { + log.error("Error on changing permissions for project " + + event.getObjectId() + " in session " + activeUserSession.getSessionId(), e); } - } catch (DBException e) { - log.error("Error on changing permissions for project " + - event.getObjectId() + " in session " + activeUserSession.getSessionId(), e); - } + }; } @Override diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java index 7cee228b18..e3c94cce88 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java @@ -24,7 +24,6 @@ import io.cloudbeaver.server.servlets.CBImageServlet; import io.cloudbeaver.server.servlets.CBStaticServlet; import io.cloudbeaver.server.servlets.CBStatusServlet; -import io.cloudbeaver.server.servlets.ProxyResourceHandler; import io.cloudbeaver.server.websockets.CBJettyWebSocketManager; import io.cloudbeaver.service.DBWServiceBindingServlet; import io.cloudbeaver.service.DBWServiceBindingWebSocket; @@ -61,6 +60,7 @@ public class CBJettyServer { } private final CBApplication application; + private Server server; public CBJettyServer(@NotNull CBApplication application) { this.application = application; @@ -69,7 +69,6 @@ public CBJettyServer(@NotNull CBApplication application) { public void runServer() { try { CBServerConfig serverConfiguration = application.getServerConfiguration(); - Server server; int serverPort = serverConfiguration.getServerPort(); String serverHost = serverConfiguration.getServerHost(); Path sslPath = getSslConfigurationPath(); @@ -101,11 +100,12 @@ public void runServer() { String rootURI = serverConfiguration.getRootURI(); servletContextHandler.setContextPath(rootURI); - ServletHolder staticServletHolder = new ServletHolder("static", new CBStaticServlet()); + ServletHolder staticServletHolder = new ServletHolder( + "static", new CBStaticServlet(Path.of(serverConfiguration.getContentRoot())) + ); staticServletHolder.setInitParameter("dirAllowed", "false"); staticServletHolder.setInitParameter("cacheControl", "public, max-age=" + CBStaticServlet.STATIC_CACHE_SECONDS); servletContextHandler.addServlet(staticServletHolder, "/"); - servletContextHandler.insertHandler(new ProxyResourceHandler(Path.of(serverConfiguration.getContentRoot()))); if (Files.isSymbolicLink(contentRootPath)) { servletContextHandler.addAliasCheck(new CBSymLinkContentAllowedAliasChecker(contentRootPath)); @@ -198,7 +198,7 @@ public void runServer() { } } } - + refreshJettyConfig(); server.start(); server.join(); } catch (Exception e) { @@ -224,6 +224,7 @@ public static void initSessionManager( ) { // Init sessions persistence CBSessionHandler sessionHandler = new CBSessionHandler(application); + sessionHandler.setRefreshCookieAge(CBSessionHandler.ONE_MINUTE); int intMaxIdleSeconds; if (maxIdleTime > Integer.MAX_VALUE) { log.warn("Max session idle time value is greater than Integer.MAX_VALUE. Integer.MAX_VALUE will be used instead"); @@ -232,6 +233,7 @@ public static void initSessionManager( intMaxIdleSeconds = (int) (maxIdleTime / 1000); log.debug("Max http session idle time: " + intMaxIdleSeconds + "s"); sessionHandler.setMaxInactiveInterval(intMaxIdleSeconds); + sessionHandler.setMaxCookieAge(intMaxIdleSeconds); DefaultSessionCache sessionCache = new DefaultSessionCache(sessionHandler); sessionCache.setSessionDataStore(new NullSessionDataStore()); @@ -241,6 +243,19 @@ public static void initSessionManager( DefaultSessionIdManager idMgr = new DefaultSessionIdManager(server); idMgr.setWorkerName(null); server.addBean(idMgr, true); + } + public synchronized void refreshJettyConfig() { + if (server == null) { + return; + } + log.info("Refreshing Jetty configuration"); + if (server.getHandler() instanceof ServletContextHandler servletContextHandler + && servletContextHandler.getSessionHandler() instanceof CBSessionHandler cbSessionHandler + ) { + cbSessionHandler.setMaxCookieAge((int) (application.getMaxSessionIdleTime() / 1000)); + var serverUrl = this.application.getServerURL(); + cbSessionHandler.setSecureCookies(serverUrl != null && serverUrl.startsWith("https://")); + } } } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSessionHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSessionHandler.java index 6fa6d0f7b8..b8539264f7 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSessionHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSessionHandler.java @@ -17,174 +17,13 @@ package io.cloudbeaver.server.jetty; import io.cloudbeaver.server.GQLApplicationAdapter; -import jakarta.servlet.SessionCookieConfig; -import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.eclipse.jetty.ee10.servlet.SessionHandler; -import java.util.Collections; -import java.util.Locale; -import java.util.Map; -import java.util.TreeMap; - public class CBSessionHandler extends SessionHandler { - private final CBCookieConfig cbCookieConfig; + static final int ONE_MINUTE = 60; private final GQLApplicationAdapter application; public CBSessionHandler(GQLApplicationAdapter application) { - this.cbCookieConfig = new CBCookieConfig(); this.application = application; } - - - @Override - public SessionCookieConfig getSessionCookieConfig() { - return this.cbCookieConfig; - } - - - //mostly copy of org.eclipse.jetty.ee10.servlet.CookieConfig but allows to use dynamic setSecure flag - public final class CBCookieConfig implements SessionCookieConfig { - - @Override - public boolean isSecure() { - var serverUrl = CBSessionHandler.this.application.getServerURL(); - return serverUrl != null && serverUrl.startsWith("https://"); - } - - @Override - public String getComment() { - return getSessionComment(); - } - - @Override - public String getDomain() { - return getSessionDomain(); - } - - @Override - public int getMaxAge() { - return getMaxCookieAge(); - } - - @Override - public void setAttribute(String name, String value) { - checkState(); - String lcase = name.toLowerCase(Locale.ENGLISH); - - switch (lcase) { - case "name" -> setName(value); - case "max-age" -> setMaxAge(value == null ? -1 : Integer.parseInt(value)); - case "comment" -> setComment(value); - case "domain" -> setDomain(value); - case "httponly" -> setHttpOnly(Boolean.parseBoolean(value)); - case "secure" -> setSecure(Boolean.parseBoolean(value)); - case "path" -> setPath(value); - default -> setSessionCookieAttribute(name, value); - } - } - - @Override - public String getAttribute(String name) { - String lcase = name.toLowerCase(Locale.ENGLISH); - return switch (lcase) { - case "name" -> getName(); - case "max-age" -> Integer.toString(getMaxAge()); - case "comment" -> getComment(); - case "domain" -> getDomain(); - case "httponly" -> String.valueOf(isHttpOnly()); - case "secure" -> String.valueOf(isSecure()); - case "path" -> getPath(); - default -> getSessionCookieAttribute(name); - }; - } - - /** - * According to the SessionCookieConfig javadoc, the attributes must also include - * all values set by explicit setters. - * - * @see SessionCookieConfig - */ - @Override - public Map getAttributes() { - Map specials = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - specials.put("name", getAttribute("name")); - specials.put("max-age", getAttribute("max-age")); - specials.put("comment", getAttribute("comment")); - specials.put("domain", getAttribute("domain")); - specials.put("httponly", getAttribute("httponly")); - specials.put("secure", getAttribute("secure")); - specials.put("path", getAttribute("path")); - specials.putAll(getSessionCookieAttributes()); - return Collections.unmodifiableMap(specials); - } - - @Override - public String getName() { - return getSessionCookie(); - } - - @Override - public String getPath() { - return getSessionPath(); - } - - @Override - public boolean isHttpOnly() { - return CBSessionHandler.this.isHttpOnly(); - } - - @Override - public void setComment(String comment) { - checkState(); - CBSessionHandler.this.setSessionComment(comment); - } - - @Override - public void setDomain(String domain) { - checkState(); - CBSessionHandler.this.setSessionDomain(domain); - } - - @Override - public void setHttpOnly(boolean httpOnly) { - checkState(); - CBSessionHandler.this.setHttpOnly(httpOnly); - } - - @Override - public void setMaxAge(int maxAge) { - checkState(); - CBSessionHandler.this.setMaxCookieAge(maxAge); - } - - @Override - public void setName(String name) { - checkState(); - CBSessionHandler.this.setSessionCookie(name); - } - - @Override - public void setPath(String path) { - checkState(); - CBSessionHandler.this.setSessionPath(path); - } - - @Override - public void setSecure(boolean secure) { - checkState(); - CBSessionHandler.this.setSecureCookies(secure); - } - - private void checkState() { - //It is allowable to call the CookieConfig.setXX methods after the SessionHandler has started, - //but before the context has fully started. Ie it is allowable for ServletContextListeners - //to call these methods in contextInitialized(). - ServletContextHandler handler = ServletContextHandler.getCurrentServletContextHandler(); - if (handler != null && handler.isAvailable()) - throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); - - } - } - - } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/PeriodicSystemJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/PeriodicSystemJob.java deleted file mode 100644 index 15d4de80d8..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/PeriodicSystemJob.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.cloudbeaver.server.jobs; - -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; -import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.model.app.DBPPlatform; -import org.jkiss.dbeaver.model.runtime.AbstractJob; -import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; - -public abstract class PeriodicSystemJob extends AbstractJob { - - @NotNull - protected final DBPPlatform platform; - private final long periodMs; - - public PeriodicSystemJob(@NotNull String name, @NotNull DBPPlatform platform, long periodMs) { - super(name); - this.platform = platform; - this.periodMs = periodMs; - - setUser(false); - setSystem(true); - } - - @Override - protected IStatus run(@NotNull DBRProgressMonitor monitor) { - if (platform.isShuttingDown()) { - return Status.OK_STATUS; - } - - doJob(monitor); - - // If the platform is still running after the job is completed, reschedule the job - if (!platform.isShuttingDown()) { - scheduleMonitor(); - } - - return Status.OK_STATUS; - } - - protected abstract void doJob(@NotNull DBRProgressMonitor monitor); - - public void scheduleMonitor() { - schedule(periodMs); - } -} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java index 354934ebc4..01aaffb15a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java @@ -21,8 +21,9 @@ import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.app.DBPPlatform; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.runtime.PeriodicJob; -public class SessionStateJob extends PeriodicSystemJob { +public class SessionStateJob extends PeriodicJob { private static final Log log = Log.getLog(SessionStateJob.class); private static final int PERIOD_MS = 30_000; // once per 30 seconds private final WebSessionManager sessionManager; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebSessionMonitorJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebSessionMonitorJob.java index c17dc270f4..e73dacdf62 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebSessionMonitorJob.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebSessionMonitorJob.java @@ -21,11 +21,12 @@ import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.app.DBPPlatform; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.runtime.PeriodicJob; /** * WebSessionMonitorJob */ -public class WebSessionMonitorJob extends PeriodicSystemJob { +public class WebSessionMonitorJob extends PeriodicJob { private static final Log log = Log.getLog(WebSessionMonitorJob.class); private static final int MONITOR_INTERVAL = 10000; // once per 10 seconds private final WebSessionManager sessionManager; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java index e558dd4c75..232d57f3f9 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java @@ -35,14 +35,23 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.eclipse.jetty.ee10.servlet.DefaultServlet; +import org.eclipse.jetty.http.HttpHeader; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.auth.SMAuthInfo; import org.jkiss.dbeaver.model.auth.SMAuthProvider; import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.IOUtils; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Map; @WebServlet(urlPatterns = "/") @@ -54,6 +63,13 @@ public class CBStaticServlet extends DefaultServlet { private static final Log log = Log.getLog(CBStaticServlet.class); + @NotNull + private final Path contentRoot; + + public CBStaticServlet(@NotNull Path contentRoot) { + this.contentRoot = contentRoot; + } + @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { for (WebServletHandlerDescriptor handler : WebHandlerRegistry.getInstance().getServletHandlers()) { @@ -83,7 +99,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t } catch (DBWebException e) { log.error("Error reading websession", e); } - super.doGet(request, response); + patchStaticContentIfNeeded(request, response); } private void performAutoLoginIfNeeded(HttpServletRequest request, WebSession webSession) { @@ -177,4 +193,40 @@ private boolean processSessionStart(HttpServletRequest request, HttpServletRespo return false; } + private void patchStaticContentIfNeeded(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String pathInContext = request.getServletPath(); + + if ("/".equals(pathInContext)) { + pathInContext = "index.html"; + } + + if (pathInContext == null || !pathInContext.endsWith("index.html") + && !pathInContext.endsWith("sso.html") + && !pathInContext.endsWith("ssoError.html") + ) { + super.doGet(request, response); + return; + } + + if (pathInContext.startsWith("/")) { + pathInContext = pathInContext.substring(1); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + var filePath = contentRoot.resolve(pathInContext); + try (InputStream fis = Files.newInputStream(filePath)) { + IOUtils.copyStream(fis, baos); + } + String indexContents = baos.toString(StandardCharsets.UTF_8); + CBServerConfig serverConfig = CBApplication.getInstance().getServerConfiguration(); + indexContents = indexContents + .replace("{ROOT_URI}", serverConfig.getRootURI()) + .replace("{STATIC_CONTENT}", serverConfig.getStaticContent()); + byte[] indexBytes = indexContents.getBytes(StandardCharsets.UTF_8); + + // Disable cache for index.html + response.setHeader(HttpHeader.CACHE_CONTROL.toString(), "no-cache, no-store, must-revalidate"); + response.setHeader(HttpHeader.EXPIRES.toString(), "0"); + response.getOutputStream().write(ByteBuffer.wrap(indexBytes)); + } + } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/ProxyResourceHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/ProxyResourceHandler.java deleted file mode 100644 index c9c20d874b..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/ProxyResourceHandler.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.cloudbeaver.server.servlets; - -import io.cloudbeaver.model.config.CBServerConfig; -import io.cloudbeaver.server.CBApplication; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Response; -import org.eclipse.jetty.util.Callback; -import org.jkiss.code.NotNull; -import org.jkiss.utils.IOUtils; - -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; - -public class ProxyResourceHandler extends Handler.Wrapper { - @NotNull - private final Path contentRoot; - - public ProxyResourceHandler(@NotNull Path contentRoot) { - this.contentRoot = contentRoot; - } - - public boolean handle(Request request, Response response, Callback callback) throws Exception { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - String pathInContext = Request.getPathInContext(request); - - if ("/".equals(pathInContext)) { - pathInContext = "index.html"; - } - - if (pathInContext == null || !pathInContext.endsWith("index.html") - && !pathInContext.endsWith("sso.html") - && !pathInContext.endsWith("ssoError.html") - ) { - return super.handle(request, response, callback); - } - - if (pathInContext.startsWith("/")) { - pathInContext = pathInContext.substring(1); - } - var filePath = contentRoot.resolve(pathInContext); - try (InputStream fis = Files.newInputStream(filePath)) { - IOUtils.copyStream(fis, baos); - } - String indexContents = baos.toString(StandardCharsets.UTF_8); - CBServerConfig serverConfig = CBApplication.getInstance().getServerConfiguration(); - indexContents = indexContents - .replace("{ROOT_URI}", serverConfig.getRootURI()) - .replace("{STATIC_CONTENT}", serverConfig.getStaticContent()); - byte[] indexBytes = indexContents.getBytes(StandardCharsets.UTF_8); - - // Disable cache for index.html - response.getHeaders().put(HttpHeader.CACHE_CONTROL.toString(), "no-cache, no-store, must-revalidate"); - response.getHeaders().put(HttpHeader.EXPIRES.toString(), "0"); - - response.write(true, ByteBuffer.wrap(indexBytes), callback); - return true; - } -} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java index 90c097cbe9..77a5a40fc7 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java @@ -26,6 +26,7 @@ import io.cloudbeaver.server.CBPlatform; import io.cloudbeaver.service.core.DBWServiceCore; import io.cloudbeaver.service.security.SMUtils; +import io.cloudbeaver.utils.WebAppUtils; import io.cloudbeaver.utils.WebConnectionFolderUtils; import io.cloudbeaver.utils.WebDataSourceUtils; import io.cloudbeaver.utils.WebEventUtils; @@ -209,7 +210,7 @@ private List getConnectionFoldersFromProject( @Override public String[] getSessionPermissions(@NotNull WebSession webSession) throws DBWebException { - if (CBApplication.getInstance().isConfigurationMode()) { + if (WebAppUtils.getWebApplication().isConfigurationMode()) { return new String[]{ DBWConstants.PERMISSION_ADMIN }; @@ -437,7 +438,7 @@ public WebConnectionInfo createConnection( var rmProject = project.getRMProject(); if (rmProject.getType() == RMProjectType.USER && !webSession.hasPermission(DBWConstants.PERMISSION_ADMIN) - && !CBApplication.getInstance().getAppConfiguration().isSupportsCustomConnections() + && !WebAppUtils.getWebApplication().getAppConfiguration().isSupportsCustomConnections() ) { throw new DBWebException("New connection create is restricted by server configuration"); } @@ -857,7 +858,7 @@ private WebConnectionInfo closeAndDeleteConnection( @Override public List getProjects(@NotNull WebSession session) { var customConnectionsEnabled = - CBApplication.getInstance().getAppConfiguration().isSupportsCustomConnections() + WebAppUtils.getWebApplication().getAppConfiguration().isSupportsCustomConnections() || SMUtils.isRMAdmin(session); return session.getAccessibleProjects().stream() .map(pr -> new WebProjectInfo(session, pr, customConnectionsEnabled)) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebDatabaseObjectInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebDatabaseObjectInfo.java index fd9569ec59..7fe7cd1d01 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebDatabaseObjectInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebDatabaseObjectInfo.java @@ -198,8 +198,7 @@ private void getObjectFeatures(DBSObject object, List features) { features.add(OBJECT_FEATURE_OBJECT_CONTAINER); try { Class childType = objectContainer.getPrimaryChildType(null); - Collection childrenCollection = objectContainer.getChildren(session.getProgressMonitor()); - if (DBSTable.class.isAssignableFrom(childType) && childrenCollection != null) { + if (DBSTable.class.isAssignableFrom(childType)) { features.add(OBJECT_FEATURE_ENTITY_CONTAINER); } } catch (Exception e) { diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalServletHandler.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalServletHandler.java index 9f31bf115f..42d2024e83 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalServletHandler.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalServletHandler.java @@ -40,7 +40,7 @@ public class LocalServletHandler extends AbstractActionServletHandler { @Override public boolean handleRequest(Servlet servlet, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException { - if (URI_PREFIX.equals(WebAppUtils.removeSideSlashes(request.getPathInfo()))) { + if (URI_PREFIX.equals(WebAppUtils.removeSideSlashes(request.getServletPath()))) { try { WebSession webSession = CBPlatform.getInstance().getSessionManager().getWebSession(request, response, true); createActionFromParams(webSession, request, response); 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 806dbd4e6b..f8ac85aa1c 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 @@ -2082,7 +2082,7 @@ public SMAuthInfo finishAuthentication(@NotNull String authId) throws DBExceptio return finishAuthentication(authInfo, false, authInfo.isForceSessionsLogout()); } - private SMAuthInfo finishAuthentication( + protected SMAuthInfo finishAuthentication( @NotNull SMAuthInfo authInfo, boolean isSyncAuth, boolean forceSessionsLogout @@ -3134,7 +3134,7 @@ private void deleteAuthSubject(Connection dbCon, String subjectId) throws SQLExc } } - private WebAuthProviderDescriptor getAuthProvider(String authProviderId) throws DBCException { + protected WebAuthProviderDescriptor getAuthProvider(String authProviderId) throws DBCException { WebAuthProviderDescriptor authProvider = WebAuthProviderRegistry.getInstance().getAuthProvider(authProviderId); if (authProvider == null) { throw new DBCException("Auth provider not found: " + authProviderId); diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java index 75ebda6796..3484e47d52 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java @@ -18,6 +18,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.Strictness; import io.cloudbeaver.auth.provider.local.LocalAuthProviderConstants; import io.cloudbeaver.model.app.WebApplication; import io.cloudbeaver.model.config.WebDatabaseConfig; @@ -301,7 +302,9 @@ CBDatabaseInitialData getInitialData() throws DBException { initialDataPath = WebAppUtils.getRelativePath( databaseConfiguration.getInitialDataConfiguration(), application.getHomeDirectory()); try (Reader reader = new InputStreamReader(new FileInputStream(initialDataPath), StandardCharsets.UTF_8)) { - Gson gson = new GsonBuilder().setLenient().create(); + Gson gson = new GsonBuilder() + .setStrictness(Strictness.LENIENT) + .create(); return gson.fromJson(reader, CBDatabaseInitialData.class); } catch (Exception e) { throw new DBException("Error loading initial data configuration", e); diff --git a/server/drivers/db2-jt400/pom.xml b/server/drivers/db2-jt400/pom.xml index 5158c8c7f2..05cdc02a0a 100644 --- a/server/drivers/db2-jt400/pom.xml +++ b/server/drivers/db2-jt400/pom.xml @@ -18,7 +18,7 @@ net.sf.jt400 jt400 - 10.5 + 20.0.7 diff --git a/server/drivers/mysql/pom.xml b/server/drivers/mysql/pom.xml index 7f993d3be7..76959e3ab1 100644 --- a/server/drivers/mysql/pom.xml +++ b/server/drivers/mysql/pom.xml @@ -20,6 +20,10 @@ mysql-connector-j 8.2.0 + + com.google.protobuf + protobuf-java + diff --git a/server/drivers/pom.xml b/server/drivers/pom.xml index 39fecc0302..06766cf06e 100644 --- a/server/drivers/pom.xml +++ b/server/drivers/pom.xml @@ -1,7 +1,12 @@ 4.0.0 - io.cloudbeaver + + io.cloudbeaver + cloudbeaver + 1.0.0-SNAPSHOT + ../ + drivers 1.0.0 pom diff --git a/server/pom.xml b/server/pom.xml index bb29dba9ae..4f9418baf2 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -25,13 +25,22 @@ bundles features - drivers - test - - - product + + + + full-build + !plain-api-server + + drivers + + product + test + + + + diff --git a/webapp/package.json b/webapp/package.json index 25dcd27415..8ff63b4aed 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -36,7 +36,7 @@ "@testing-library/user-event": "^14", "@types/react": "^18", "@types/react-dom": "^18", - "concurrently": "^8", + "concurrently": "^9", "husky": "^9", "lerna": "^5", "mobx": "^6", diff --git a/webapp/packages/core-app/src/AppScreen/RightArea.tsx b/webapp/packages/core-app/src/AppScreen/RightArea.tsx index 540a728ea2..34097d383f 100644 --- a/webapp/packages/core-app/src/AppScreen/RightArea.tsx +++ b/webapp/packages/core-app/src/AppScreen/RightArea.tsx @@ -40,8 +40,12 @@ export const RightArea = observer(function RightArea({ className }) { const toolsDisabled = appScreenService.rightAreaBottom.getDisplayed({}).length === 0; + function close() { + optionsPanelService.close(); + } + return ( - + @@ -61,7 +65,7 @@ export const RightArea = observer(function RightArea({ className }) { - optionsPanelService.close()} /> + ); diff --git a/webapp/packages/core-blocks/package.json b/webapp/packages/core-blocks/package.json index 8e274afb71..4448edf506 100644 --- a/webapp/packages/core-blocks/package.json +++ b/webapp/packages/core-blocks/package.json @@ -34,6 +34,7 @@ "mobx": "^6", "mobx-react-lite": "^4", "react": "^18", + "react-hotkeys-hook": "^4", "reakit": "^1", "reakit-utils": "^0" }, diff --git a/webapp/packages/core-blocks/src/Button.tsx b/webapp/packages/core-blocks/src/Button.tsx index 91be6997bf..5ba253b03b 100644 --- a/webapp/packages/core-blocks/src/Button.tsx +++ b/webapp/packages/core-blocks/src/Button.tsx @@ -72,12 +72,6 @@ export const Button = observer(function Button({ ['click'], ); - function handleEnter(event: React.KeyboardEvent) { - if (event.key === 'Enter') { - event.currentTarget.click(); - } - } - loading = state.loading || loading; if (loading) { @@ -89,7 +83,6 @@ export const Button = observer(function Button({