From 36c089ed066fde784fc3b4d80532f689dc4a24ca Mon Sep 17 00:00:00 2001 From: Alexander Skoblikov Date: Thu, 8 Aug 2024 22:12:50 +0300 Subject: [PATCH] CB-5375 auto migration to postgres DB for default CB EE configuration (#2809) * CB-5375 auto migration to postgres DB for default CB EE configuration * CB-5375 fix test * CB-5375 move legacy db * CB-5474 async qm data migration * CB-5375 comment unused field --------- Co-authored-by: Evgenia Bezborodova <139753579+EvgeniaBzzz@users.noreply.github.com> --- .../src/io/cloudbeaver/DBWebException.java | 1 + .../cloudbeaver/model/app/WebApplication.java | 4 ++ .../schema/service.core.graphqls | 1 + .../DBWebExceptionServerNotInitialized.java | 27 +++++++++++ .../src/io/cloudbeaver/WebAction.java | 2 + .../io/cloudbeaver/server/CBApplication.java | 28 ++++++++++- .../CBServerConfigurationController.java | 9 ++-- .../service/WebServiceBindingBase.java | 7 ++- .../service/core/DBWServiceCore.java | 2 +- .../META-INF/MANIFEST.MF | 2 +- .../EmbeddedSecurityControllerFactory.java | 47 ++++++++++++++----- .../service/security/db/CBDatabase.java | 20 ++++++-- .../security/db/WebDatabaseConfig.java | 7 +-- .../test/platform/SQLQueryTranslatorTest.java | 6 ++- 14 files changed, 134 insertions(+), 29 deletions(-) create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWebExceptionServerNotInitialized.java diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWebException.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWebException.java index e0e278a13f..f4cc8e50ea 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWebException.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWebException.java @@ -38,6 +38,7 @@ public class DBWebException extends DBException implements GraphQLError { public static final String ERROR_CODE_SESSION_EXPIRED = "sessionExpired"; public static final String ERROR_CODE_ACCESS_DENIED = "accessDenied"; + public static final String ERROR_CODE_SERVER_NOT_INITIALIZED = "serverNotInitialized"; public static final String ERROR_CODE_LICENSE_DENIED = "licenseRequired"; public static final String ERROR_CODE_IDENT_REQUIRED = "identRequired"; public static final String ERROR_CODE_AUTH_REQUIRED = "authRequired"; diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java index 6e14a4e705..6e5a2ccf49 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java @@ -42,6 +42,10 @@ public interface WebApplication extends DBPApplication { boolean isConfigurationMode(); + default boolean isInitializationMode() { + return false; + } + WebAppConfiguration getAppConfiguration(); WebServerConfiguration getServerConfiguration(); diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index 622b05fa23..7bdc0106e7 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -148,6 +148,7 @@ type ServerConfig { localHostAddress: String configurationMode: Boolean! + # initializationMode: Boolean! @since(version: "24.1.5") developmentMode: Boolean! redirectOnFederatedAuth: Boolean! distributed: Boolean! diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWebExceptionServerNotInitialized.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWebExceptionServerNotInitialized.java new file mode 100644 index 0000000000..610d7c414e --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWebExceptionServerNotInitialized.java @@ -0,0 +1,27 @@ +/* + * 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; + +/** + * "Access denied" exception + */ +public class DBWebExceptionServerNotInitialized extends DBWebException { + + public DBWebExceptionServerNotInitialized(String message) { + super(message, ERROR_CODE_SERVER_NOT_INITIALIZED); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebAction.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebAction.java index b0aa55ea64..873864f142 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebAction.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebAction.java @@ -33,4 +33,6 @@ boolean authRequired() default true; + boolean initializationRequired() default true; + } 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 ff1d0d6017..32cc1ac346 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 @@ -69,6 +69,7 @@ import java.security.Policy; import java.security.ProtectionDomain; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; /** * This class controls all aspects of the application's execution @@ -108,6 +109,8 @@ public static CBApplication getInstance() { private WebSessionManager sessionManager; + private final Map initActions = new ConcurrentHashMap<>(); + public CBApplication() { this.homeDirectory = new File(initHomeFolder()); } @@ -552,7 +555,8 @@ public synchronized void finishConfiguration( reloadConfiguration(credentialsProvider); } - public synchronized void reloadConfiguration(@Nullable SMCredentialsProvider credentialsProvider) throws DBException { + public synchronized void reloadConfiguration(@Nullable SMCredentialsProvider credentialsProvider) + throws DBException { // Re-load runtime configuration try { Path runtimeAppConfigPath = getServerConfigurationController().getRuntimeAppConfigPath(); @@ -717,7 +721,10 @@ public Class getPlatformUIClass() { return CBPlatformUI.class; } - public void saveProductConfiguration(SMCredentialsProvider credentialsProvider, Map productConfiguration) throws DBException { + public void saveProductConfiguration( + SMCredentialsProvider credentialsProvider, + Map productConfiguration + ) throws DBException { getServerConfigurationController().saveProductConfiguration(productConfiguration); flushConfiguration(credentialsProvider); sendConfigChangedEvent(credentialsProvider); @@ -750,4 +757,21 @@ private void refreshDisabledDriversConfig() { public boolean isEnvironmentVariablesAccessible() { return getAppConfiguration().isSystemVariablesResolvingEnabled(); } + + @Override + public boolean isInitializationMode() { + return !initActions.isEmpty(); + } + + public void addInitAction(@NotNull String actionId, @NotNull String description) { + initActions.put(actionId, description); + } + + public void removeInitAction(@NotNull String actionId) { + initActions.remove(actionId); + } + + public Map getInitActions() { + return Map.copyOf(initActions); + } } 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 40d925de13..b4233baf3f 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 @@ -330,7 +330,7 @@ protected GsonBuilder getGsonBuilder() { .registerTypeAdapter(PasswordPolicyConfiguration.class, smPasswordPoliceConfigCreator); } - protected void saveRuntimeConfig(SMCredentialsProvider credentialsProvider) throws DBException { + public synchronized void saveRuntimeConfig(SMCredentialsProvider credentialsProvider) throws DBException { saveRuntimeConfig( serverConfiguration, appConfiguration, @@ -338,7 +338,7 @@ protected void saveRuntimeConfig(SMCredentialsProvider credentialsProvider) thro ); } - protected void saveRuntimeConfig( + protected synchronized void saveRuntimeConfig( @NotNull CBServerConfig serverConfig, @NotNull CBAppConfig appConfig, SMCredentialsProvider credentialsProvider @@ -350,7 +350,7 @@ protected void saveRuntimeConfig( writeRuntimeConfig(getRuntimeAppConfigPath(), configurationProperties); } - private void writeRuntimeConfig(Path runtimeConfigPath, Map configurationProperties) + private synchronized void writeRuntimeConfig(Path runtimeConfigPath, Map configurationProperties) throws DBException { if (Files.exists(runtimeConfigPath)) { ContentUtils.makeFileBackup(runtimeConfigPath); @@ -370,7 +370,8 @@ private void writeRuntimeConfig(Path runtimeConfigPath, Map conf } - public void updateServerUrl(@NotNull SMCredentialsProvider credentialsProvider, @Nullable String newPublicUrl) throws DBException { + public synchronized void updateServerUrl(@NotNull SMCredentialsProvider credentialsProvider, + @Nullable String newPublicUrl) throws DBException { getServerConfiguration().setServerURL(newPublicUrl); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java index ac61c1f9c0..7a85a39e97 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java @@ -250,6 +250,12 @@ private void checkServicePermissions(Method method, WebActionSet actionSet) thro } private void checkActionPermissions(@NotNull Method method, @NotNull WebAction webAction) throws DBWebException { + CBApplication application = CBApplication.getInstance(); + if (application.isInitializationMode() && webAction.initializationRequired()) { + String message = "Server initialization in progress: " + + String.join(",", application.getInitActions().values()) + ".\nDo not restart the server."; + throw new DBWebExceptionServerNotInitialized(message); + } String[] reqPermissions = webAction.requirePermissions(); if (reqPermissions.length == 0 && !webAction.authRequired()) { return; @@ -258,7 +264,6 @@ private void checkActionPermissions(@NotNull Method method, @NotNull WebAction w if (session == null) { throw new DBWebExceptionAccessDenied("No open session - anonymous access restricted"); } - CBApplication application = CBApplication.getInstance(); if (!application.isConfigurationMode()) { if (webAction.authRequired() && !session.isAuthorizedInSecurityManager()) { log.debug("Anonymous access to " + method.getName() + " restricted"); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java index cabced5886..48f78fbe7a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java @@ -38,7 +38,7 @@ */ public interface DBWServiceCore extends DBWService { - @WebAction(authRequired = false) + @WebAction(authRequired = false, initializationRequired = false) WebServerConfig getServerConfig() throws DBWebException; @WebAction(authRequired = false) diff --git a/server/bundles/io.cloudbeaver.service.security/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.security/META-INF/MANIFEST.MF index 7dd72ecd6c..a7d81b179c 100644 --- a/server/bundles/io.cloudbeaver.service.security/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.security/META-INF/MANIFEST.MF @@ -13,7 +13,7 @@ Require-Bundle: org.jkiss.dbeaver.model;visibility:=reexport, org.jkiss.dbeaver.model.sql, org.jkiss.dbeaver.model.sql.jdbc, org.jkiss.dbeaver.registry;visibility:=reexport, - org.jkiss.bundle.apache.dbcp, + org.jkiss.bundle.apache.dbcp;visibility:=reexport, io.cloudbeaver.model Export-Package: io.cloudbeaver.auth.provider.local, io.cloudbeaver.auth.provider.rp, diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/EmbeddedSecurityControllerFactory.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/EmbeddedSecurityControllerFactory.java index 6b5992918c..6c7b581b13 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/EmbeddedSecurityControllerFactory.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/EmbeddedSecurityControllerFactory.java @@ -16,10 +16,12 @@ */ package io.cloudbeaver.service.security; +import io.cloudbeaver.auth.NoAuthCredentialsProvider; import io.cloudbeaver.model.app.WebAuthApplication; import io.cloudbeaver.service.security.db.CBDatabase; import io.cloudbeaver.service.security.db.WebDatabaseConfig; import io.cloudbeaver.service.security.internal.ClearAuthAttemptInfoJob; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; @@ -36,7 +38,7 @@ public static CBDatabase getDbInstance() { /** * Create new security controller instance with custom configuration */ - public CBEmbeddedSecurityController createSecurityService( + public CBEmbeddedSecurityController createSecurityService( T application, WebDatabaseConfig databaseConfig, SMCredentialsProvider credentialsProvider, @@ -45,32 +47,53 @@ public CBEmbeddedSecurityController createSecurityService( if (DB_INSTANCE == null) { synchronized (EmbeddedSecurityControllerFactory.class) { if (DB_INSTANCE == null) { - DB_INSTANCE = new CBDatabase(application, databaseConfig); + DB_INSTANCE = createAndInitDatabaseInstance( + application, + databaseConfig, + smConfig + ); } } - var securityController = createEmbeddedSecurityController( - application, DB_INSTANCE, credentialsProvider, smConfig - ); - //FIXME circular dependency - DB_INSTANCE.setAdminSecurityController(securityController); - DB_INSTANCE.initialize(); + if (application.isLicenseRequired()) { // delete expired auth info job in enterprise products - new ClearAuthAttemptInfoJob(securityController).schedule(); + new ClearAuthAttemptInfoJob(createEmbeddedSecurityController( + application, DB_INSTANCE, new NoAuthCredentialsProvider(), smConfig + )).schedule(); } - return securityController; } return createEmbeddedSecurityController( application, DB_INSTANCE, credentialsProvider, smConfig ); } - protected CBEmbeddedSecurityController createEmbeddedSecurityController( + protected @NotNull CBDatabase createAndInitDatabaseInstance( + @NotNull T application, + @NotNull WebDatabaseConfig databaseConfig, + @NotNull SMControllerConfiguration smConfig + ) throws DBException { + var database = new CBDatabase(application, databaseConfig); + var securityController = createEmbeddedSecurityController( + application, database, new NoAuthCredentialsProvider(), smConfig + ); + //FIXME circular dependency + database.setAdminSecurityController(securityController); + try { + database.initialize(); + } catch (DBException e) { + database.shutdown(); + throw e; + } + + return database; + } + + protected CBEmbeddedSecurityController createEmbeddedSecurityController( T application, CBDatabase database, SMCredentialsProvider credentialsProvider, SMControllerConfiguration smConfig ) { - return new CBEmbeddedSecurityController(application, database, credentialsProvider, smConfig); + return new CBEmbeddedSecurityController(application, database, credentialsProvider, smConfig); } } 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 ea1a858221..0c373781e4 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 @@ -475,7 +475,7 @@ public void close() throws SQLException { // Persistence - private void validateInstancePersistentState(Connection connection) throws IOException, SQLException, DBException { + protected void validateInstancePersistentState(Connection connection) throws IOException, SQLException, DBException { try (JDBCTransaction txn = new JDBCTransaction(connection)) { checkInstanceRecord(connection); var defaultTeamId = application.getAppConfiguration().getDefaultUserTeam(); @@ -587,10 +587,24 @@ public SQLDialect getDialect() { } public static boolean isDefaultH2Configuration(WebDatabaseConfig databaseConfiguration) { - var v1DefaultUrl = "jdbc:h2:/opt/cloudbeaver/workspace/.data/" + V1_DB_NAME; - var v2DefaultUrl = "jdbc:h2:/opt/cloudbeaver/workspace/.data/" + V2_DB_NAME; + var workspace = WebAppUtils.getWebApplication().getWorkspaceDirectory(); + var v1Path = workspace.resolve(".data").resolve(V1_DB_NAME); + var v2Path = workspace.resolve(".data").resolve(V2_DB_NAME); + var v1DefaultUrl = "jdbc:h2:" + v1Path; + var v2DefaultUrl = "jdbc:h2:" + v2Path; return v1DefaultUrl.equals(databaseConfiguration.getUrl()) || v2DefaultUrl.equals(databaseConfiguration.getUrl()); } + protected WebDatabaseConfig getDatabaseConfiguration() { + return databaseConfiguration; + } + + protected WebApplication getApplication() { + return application; + } + + protected SMAdminController getAdminSecurityController() { + return adminSecurityController; + } } diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/WebDatabaseConfig.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/WebDatabaseConfig.java index fb7243462a..27bf86f04f 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/WebDatabaseConfig.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/WebDatabaseConfig.java @@ -63,6 +63,7 @@ public String getUser() { return user; } + @Override public String getPassword() { return password; @@ -85,15 +86,15 @@ public String getSchema() { return schema; } - void setPassword(String password) { + public void setPassword(String password) { this.password = password; } - void setSchema(String schema) { + public void setSchema(String schema) { this.schema = schema; } - void setUser(String user) { + public void setUser(String user) { this.user = user; } } diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/SQLQueryTranslatorTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/SQLQueryTranslatorTest.java index 3c06bbce97..416aaa8656 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/SQLQueryTranslatorTest.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/SQLQueryTranslatorTest.java @@ -162,8 +162,10 @@ public void createTableWithAutoincrement() throws DBException { Map expectedSqlByDialect = new HashMap<>(); expectedSqlByDialect.put(new H2SQLDialect(), basicSql); expectedSqlByDialect.put(new PostgreDialect(), "CREATE SEQUENCE CB_TEST_TYPES_AUTOINC_COLUMN;\n" + - "CREATE TABLE CB_TEST_TYPES (AUTOINC_COLUMN BIGINT NOT NULL DEFAULT NEXTVAL('CB_TEST_TYPES_AUTOINC_COLUMN'));" + - "\n"); + "CREATE TABLE CB_TEST_TYPES (AUTOINC_COLUMN BIGINT NOT NULL DEFAULT NEXTVAL" + + "('CB_TEST_TYPES_AUTOINC_COLUMN'));\n" + + "ALTER SEQUENCE CB_TEST_TYPES_AUTOINC_COLUMN OWNED BY CB_TEST_TYPES.AUTOINC_COLUMN;\n" + ); expectedSqlByDialect.put(new MySQLDialect(), basicSql); expectedSqlByDialect.put(