From 1d4fa4941fa51870e2143150b00c6c33a4ac3616 Mon Sep 17 00:00:00 2001 From: DarioGii Date: Mon, 23 Dec 2024 10:40:25 +0000 Subject: [PATCH] 2270: renamed datasource config flag --- build.gradle | 23 ++++ .../docker-compose-latest-fat-security.yml | 2 +- ...ocker-compose-latest-security-with-sso.yml | 2 +- .../docker-compose-latest-security.yml | 2 +- ...ker-compose-latest-ultra-lite-security.yml | 2 +- .../database/DatabaseBackupHelper.java | 46 ++++--- .../security/database/DatabaseConfig.java | 117 ++++++++++++++++++ .../SPDF/model/ApplicationProperties.java | 56 +++++---- src/main/resources/settings.yml.template | 11 +- .../security/database/DatabaseConfigTest.java | 75 +++++++++++ .../SPDFApplicationIntegrationTest.java | 59 +++++++++ 11 files changed, 349 insertions(+), 46 deletions(-) create mode 100644 src/main/java/stirling/software/SPDF/config/security/database/DatabaseConfig.java create mode 100644 src/test/java/stirling/software/SPDF/config/security/database/DatabaseConfigTest.java create mode 100644 src/test/java/stirling/software/SPDF/integrationtests/SPDFApplicationIntegrationTest.java diff --git a/build.gradle b/build.gradle index 4f509df5..15323412 100644 --- a/build.gradle +++ b/build.gradle @@ -72,6 +72,29 @@ sourceSets { } } + + test { + java { + if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") { + exclude "stirling/software/SPDF/config/security/**" + exclude "stirling/software/SPDF/controller/api/UserControllerTest.java" + exclude "stirling/software/SPDF/controller/api/DatabaseControllerTest.java" + exclude "stirling/software/SPDF/controller/web/AccountWebControllerTest.java" + exclude "stirling/software/SPDF/controller/web/DatabaseWebControllerTest.java" + exclude "stirling/software/SPDF/model/ApiKeyAuthenticationTokenTest.java" + exclude "stirling/software/SPDF/model/AttemptCounterTest.java" + exclude "stirling/software/SPDF/model/AuthorityTest.java" + exclude "stirling/software/SPDF/model/PersistentLoginTest.java" + exclude "stirling/software/SPDF/model/SessionEntityTest.java" + exclude "stirling/software/SPDF/model/UserTest.java" + exclude "stirling/software/SPDF/repository/**" + } + + if (System.getenv("STIRLING_PDF_DESKTOP_UI") == "false") { + exclude "stirling/software/SPDF/UI/impl/**" + } + } + } } openApi { diff --git a/exampleYmlFiles/docker-compose-latest-fat-security.yml b/exampleYmlFiles/docker-compose-latest-fat-security.yml index 8b7ad305..591091b0 100644 --- a/exampleYmlFiles/docker-compose-latest-fat-security.yml +++ b/exampleYmlFiles/docker-compose-latest-fat-security.yml @@ -34,7 +34,7 @@ services: SYSTEM_DATASOURCE_HOSTNAME: "db" SYSTEM_DATASOURCE_PORT: "5432" SYSTEM_DATASOURCE_NAME: "stirling_pdf" - SYSTEM_DATASOURCE_USEDEFAULT: "false" + SYSTEM_DATASOURCE_ENABLECUSTOMDATABASE: "false" SYSTEM_DATASOURCE_USERNAME: "admin" SYSTEM_DATASOURCE_PASSWORD: "stirling" restart: on-failure:5 diff --git a/exampleYmlFiles/docker-compose-latest-security-with-sso.yml b/exampleYmlFiles/docker-compose-latest-security-with-sso.yml index 90fec34d..fd399c5e 100644 --- a/exampleYmlFiles/docker-compose-latest-security-with-sso.yml +++ b/exampleYmlFiles/docker-compose-latest-security-with-sso.yml @@ -42,7 +42,7 @@ services: SYSTEM_DATASOURCE_HOSTNAME: "db" SYSTEM_DATASOURCE_PORT: "5432" SYSTEM_DATASOURCE_NAME: "stirling_pdf" - SYSTEM_DATASOURCE_USEDEFAULT: "false" + SYSTEM_DATASOURCE_ENABLECUSTOMDATABASE: "false" SYSTEM_DATASOURCE_USERNAME: "admin" SYSTEM_DATASOURCE_PASSWORD: "stirling" restart: on-failure:5 diff --git a/exampleYmlFiles/docker-compose-latest-security.yml b/exampleYmlFiles/docker-compose-latest-security.yml index 89124a81..ae346d78 100644 --- a/exampleYmlFiles/docker-compose-latest-security.yml +++ b/exampleYmlFiles/docker-compose-latest-security.yml @@ -36,7 +36,7 @@ services: SYSTEM_DATASOURCE_HOSTNAME: "db" SYSTEM_DATASOURCE_PORT: "5432" SYSTEM_DATASOURCE_NAME: "stirling_pdf" - SYSTEM_DATASOURCE_USEDEFAULT: "false" + SYSTEM_DATASOURCE_ENABLECUSTOMDATABASE: "false" SYSTEM_DATASOURCE_USERNAME: "admin" SYSTEM_DATASOURCE_PASSWORD: "stirling" restart: on-failure:5 diff --git a/exampleYmlFiles/docker-compose-latest-ultra-lite-security.yml b/exampleYmlFiles/docker-compose-latest-ultra-lite-security.yml index 9ce762e5..881c4330 100644 --- a/exampleYmlFiles/docker-compose-latest-ultra-lite-security.yml +++ b/exampleYmlFiles/docker-compose-latest-ultra-lite-security.yml @@ -31,7 +31,7 @@ services: SYSTEM_DATASOURCE_HOSTNAME: "db" SYSTEM_DATASOURCE_PORT: "5432" SYSTEM_DATASOURCE_NAME: "stirling_pdf" - SYSTEM_DATASOURCE_USEDEFAULT: "false" + SYSTEM_DATASOURCE_ENABLECUSTOMDATABASE: "false" SYSTEM_DATASOURCE_USERNAME: "admin" SYSTEM_DATASOURCE_PASSWORD: "stirling" restart: on-failure:5 diff --git a/src/main/java/stirling/software/SPDF/config/security/database/DatabaseBackupHelper.java b/src/main/java/stirling/software/SPDF/config/security/database/DatabaseBackupHelper.java index 8d52b3dd..7af08c99 100644 --- a/src/main/java/stirling/software/SPDF/config/security/database/DatabaseBackupHelper.java +++ b/src/main/java/stirling/software/SPDF/config/security/database/DatabaseBackupHelper.java @@ -18,6 +18,8 @@ import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; +import javax.sql.DataSource; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.PathResource; import org.springframework.core.io.support.EncodedResource; @@ -29,7 +31,6 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.interfaces.DatabaseInterface; import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.exception.BackupNotFoundException; -import stirling.software.SPDF.model.provider.UnsupportedProviderException; import stirling.software.SPDF.utils.FileInfo; @Slf4j @@ -40,7 +41,14 @@ public class DatabaseService implements DatabaseInterface { public static final String SQL_SUFFIX = ".sql"; private static final String BACKUP_DIR = "configs/db/backup/"; - @Autowired private DatabaseConfig databaseConfig; + private final ApplicationProperties applicationProperties; + private final DataSource dataSource; + + @Autowired + public DatabaseService(ApplicationProperties applicationProperties, DataSource dataSource) { + this.applicationProperties = applicationProperties; + this.dataSource = dataSource; + } /** * Checks if there is at least one backup @@ -133,7 +141,7 @@ public class DatabaseService implements DatabaseInterface { /** Filter and delete old backups if there are more than 5 */ @Override - public void exportDatabase() throws SQLException, UnsupportedProviderException { + public void exportDatabase() throws SQLException { List filteredBackupList = this.getBackupList().stream() .filter(backup -> !backup.getFileName().startsWith(BACKUP_PREFIX + "user_")) @@ -148,12 +156,12 @@ public class DatabaseService implements DatabaseInterface { Path insertOutputFilePath = this.getBackupFilePath(BACKUP_PREFIX + dateNow.format(myFormatObj) + SQL_SUFFIX); - try (Connection conn = databaseConfig.connection()) { + try (Connection conn = dataSource.getConnection()) { ScriptUtils.executeSqlScript( conn, new EncodedResource(new PathResource(insertOutputFilePath))); log.info("Database export completed: {}", insertOutputFilePath); - } catch (SQLException | UnsupportedProviderException e) { + } catch (SQLException e) { log.error("Error during database export: {}", e.getMessage(), e); throw e; } catch (ScriptException e) { @@ -184,13 +192,12 @@ public class DatabaseService implements DatabaseInterface { public String getH2Version() { String version = "Unknown"; - if (databaseConfig - .getApplicationProperties() + if (applicationProperties .getSystem() .getDatasource() .getType() .equals(ApplicationProperties.Driver.H2.name())) { - try (Connection conn = databaseConfig.connection()) { + try (Connection conn = dataSource.getConnection()) { try (Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT H2VERSION() AS version")) { if (rs.next()) { @@ -198,7 +205,7 @@ public class DatabaseService implements DatabaseInterface { log.info("H2 Database Version: {}", version); } } - } catch (SQLException | UnsupportedProviderException e) { + } catch (SQLException e) { log.error("Error retrieving H2 version: {}", e.getMessage(), e); } } @@ -240,14 +247,21 @@ public class DatabaseService implements DatabaseInterface { } private void executeDatabaseScript(Path scriptPath) { - try (Connection conn = databaseConfig.connection()) { - ScriptUtils.executeSqlScript(conn, new EncodedResource(new PathResource(scriptPath))); + if (applicationProperties + .getSystem() + .getDatasource() + .getType() + .equals(ApplicationProperties.Driver.H2.name())) { + try (Connection conn = dataSource.getConnection()) { + ScriptUtils.executeSqlScript( + conn, new EncodedResource(new PathResource(scriptPath))); - log.info("Database import completed: {}", scriptPath); - } catch (SQLException | UnsupportedProviderException e) { - log.error("Error during database import: {}", e.getMessage(), e); - } catch (ScriptException e) { - log.error("Error: File {} not found", scriptPath.toString(), e); + log.info("Database import completed: {}", scriptPath); + } catch (SQLException e) { + log.error("Error during database import: {}", e.getMessage(), e); + } catch (ScriptException e) { + log.error("Error: File {} not found", scriptPath.toString(), e); + } } } diff --git a/src/main/java/stirling/software/SPDF/config/security/database/DatabaseConfig.java b/src/main/java/stirling/software/SPDF/config/security/database/DatabaseConfig.java new file mode 100644 index 00000000..d68cadc6 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/database/DatabaseConfig.java @@ -0,0 +1,117 @@ +package stirling.software.SPDF.config.security.database; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import stirling.software.SPDF.model.ApplicationProperties; +import stirling.software.SPDF.model.provider.UnsupportedProviderException; + +@Slf4j +@Getter +@Configuration +public class DatabaseConfig { + + public static final String DATASOURCE_DEFAULT_URL = + "jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"; + public static final String DATASOURCE_URL_TEMPLATE = "jdbc:%s://%s:%4d/%s"; + public static final String DEFAULT_DRIVER = "org.h2.Driver"; + public static final String DEFAULT_USERNAME = "sa"; + public static final String POSTGRES_DRIVER = "org.postgresql.Driver"; + + private final ApplicationProperties applicationProperties; + + @Autowired + public DatabaseConfig(ApplicationProperties applicationProperties) { + this.applicationProperties = applicationProperties; + } + + /** + * Creates the DataSource for the connection to the DB. If useDefault + * is set to true, it will use the default H2 DB. If it is set to false + * , it will use the user's custom configuration set in the settings.yml. + * + * @return a DataSource using the configuration settings in the settings.yml + * @throws UnsupportedProviderException if the type of database selected is not supported + */ + @Bean + public DataSource dataSource() throws UnsupportedProviderException { + ApplicationProperties.System system = applicationProperties.getSystem(); + ApplicationProperties.Datasource datasource = system.getDatasource(); + DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create(); + + if (datasource.isEnableCustomDatabase()) { + log.debug("Using default H2 database"); + + dataSourceBuilder.driverClassName(DEFAULT_DRIVER); + dataSourceBuilder.url(DATASOURCE_DEFAULT_URL); + dataSourceBuilder.username(DEFAULT_USERNAME); + + return dataSourceBuilder.build(); + } + + dataSourceBuilder.driverClassName(getDriverClassName(datasource.getType())); + dataSourceBuilder.url( + getDataSourceUrl( + datasource.getType(), + datasource.getHostName(), + datasource.getPort(), + datasource.getName())); + dataSourceBuilder.username(datasource.getUsername()); + dataSourceBuilder.password(datasource.getPassword()); + + return dataSourceBuilder.build(); + } + + /** + * Generate the URL the DataSource will use to connect to the database + * + * @param dataSourceType the type of the database + * @param hostname the host name + * @param port the port number to use for the database + * @param dataSourceName the name the database to connect to + * @return the DataSource URL + */ + private String getDataSourceUrl( + String dataSourceType, String hostname, Integer port, String dataSourceName) { + return DATASOURCE_URL_TEMPLATE.formatted(dataSourceType, hostname, port, dataSourceName); + } + + /** + * Selects the database driver based on the type of database chosen. + * + * @param driverName the type of the driver (e.g. 'h2', 'postgresql') + * @return the fully qualified driver for the database chosen + * @throws UnsupportedProviderException when an unsupported database is selected + */ + private String getDriverClassName(String driverName) throws UnsupportedProviderException { + try { + ApplicationProperties.Driver driver = + ApplicationProperties.Driver.valueOf(driverName.toUpperCase()); + + switch (driver) { + case H2 -> { + log.debug("H2 driver selected"); + return DEFAULT_DRIVER; + } + case POSTGRESQL -> { + log.debug("Postgres driver selected"); + return POSTGRES_DRIVER; + } + default -> { + log.warn("{} driver selected", driverName); + throw new UnsupportedProviderException( + driverName + " is not currently supported"); + } + } + } catch (IllegalArgumentException e) { + log.warn("Unknown driver: {}", driverName); + throw new UnsupportedProviderException(driverName + " is not currently supported"); + } + } +} diff --git a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java index c5546ceb..375a316a 100644 --- a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java +++ b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java @@ -8,7 +8,6 @@ import java.net.HttpURLConnection; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.sql.Driver; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -113,23 +112,6 @@ public class ApplicationProperties { return saml2.getEnabled() || oauth2.getEnabled(); } - public boolean isUserPass() { - return (loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()) - || loginMethod.equalsIgnoreCase(LoginMethods.ALL.toString())); - } - - public boolean isOauth2Activ() { - return (oauth2 != null - && oauth2.getEnabled() - && !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())); - } - - public boolean isSaml2Activ() { - return (saml2 != null - && saml2.getEnabled() - && !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())); - } - public enum LoginMethods { ALL("all"), NORMAL("normal"), @@ -148,6 +130,23 @@ public class ApplicationProperties { } } + public boolean isUserPass() { + return (loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()) + || loginMethod.equalsIgnoreCase(LoginMethods.ALL.toString())); + } + + public boolean isOauth2Activ() { + return (oauth2 != null + && oauth2.getEnabled() + && !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())); + } + + public boolean isSaml2Activ() { + return (saml2 != null + && saml2.getEnabled() + && !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())); + } + @Data public static class InitialLogin { private String username; @@ -288,23 +287,36 @@ public class ApplicationProperties { @Data public static class Datasource { - private String url; - private Driver driver; + private boolean enableCustomDatabase; + private String type; + private String hostName; + private Integer port; + private String name; private String username; - private String password; + @ToString.Exclude private String password; } public enum Driver { H2("h2"), POSTGRESQL("postgresql"), ORACLE("oracle"), - MY_SQL("mysql"); + MYSQL("mysql"); private final String driverName; Driver(String driverName) { this.driverName = driverName; } + + @Override + public String toString() { + return """ + Driver { + driverName='%s' + } + """ + .formatted(driverName); + } } @Data diff --git a/src/main/resources/settings.yml.template b/src/main/resources/settings.yml.template index dcd94080..8942b0b1 100644 --- a/src/main/resources/settings.yml.template +++ b/src/main/resources/settings.yml.template @@ -86,10 +86,13 @@ system: tessdataDir: /usr/share/tessdata # path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored. enableAnalytics: undefined # set to 'true' to enable analytics, set to 'false' to disable analytics; for enterprise users, this is set to true datasource: - url: jdbc:postgresql://localhost:5432/stirling-pdf-DB - driver: postgresql - username: postgres - password: + enableCustomDatabase: true # set this property to 'true' if you would like to use the default database configuration + type: postgresql # the type of the database to set (e.g. 'h2', 'postgresql') + hostName: localhost # the host name to use for the database url. Set to 'localhost' when running the app locally. Set to match the name of the container name of your database container when running the app on a server (Docker configuration) + port: 5432 # set the port number of the database. Ensure this matches the port the database is listening to + name: postgres # set the name of your database. Should match the name of the database you create + username: postgres # set the database username + password: postgres # set the database password ui: appName: '' # application's visible name diff --git a/src/test/java/stirling/software/SPDF/config/security/database/DatabaseConfigTest.java b/src/test/java/stirling/software/SPDF/config/security/database/DatabaseConfigTest.java new file mode 100644 index 00000000..db5c1bab --- /dev/null +++ b/src/test/java/stirling/software/SPDF/config/security/database/DatabaseConfigTest.java @@ -0,0 +1,75 @@ +package stirling.software.SPDF.config.security.database; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import stirling.software.SPDF.model.ApplicationProperties; +import stirling.software.SPDF.model.provider.UnsupportedProviderException; + +import javax.sql.DataSource; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class DatabaseConfigTest { + + @Mock + private ApplicationProperties applicationProperties; + + @InjectMocks + private DatabaseConfig databaseConfig; + + @Test + void testDefaultConfigurationForDataSource() throws UnsupportedProviderException { + var system = mock(ApplicationProperties.System.class); + var datasource = mock(ApplicationProperties.Datasource.class); + + when(applicationProperties.getSystem()).thenReturn(system); + when(system.getDatasource()).thenReturn(datasource); + when(datasource.isEnableCustomDatabase()).thenReturn(true); + + var result = databaseConfig.dataSource(); + + assertInstanceOf(DataSource.class, result); + } + + @Test + void testCustomConfigurationForDataSource() throws UnsupportedProviderException { + var system = mock(ApplicationProperties.System.class); + var datasource = mock(ApplicationProperties.Datasource.class); + + when(applicationProperties.getSystem()).thenReturn(system); + when(system.getDatasource()).thenReturn(datasource); + when(datasource.isEnableCustomDatabase()).thenReturn(false); + when(datasource.getType()).thenReturn("postgresql"); + when(datasource.getHostName()).thenReturn("localhost"); + when(datasource.getPort()).thenReturn(5432); + when(datasource.getName()).thenReturn("postgres"); + when(datasource.getUsername()).thenReturn("postgres"); + when(datasource.getPassword()).thenReturn("postgres"); + + var result = databaseConfig.dataSource(); + + assertInstanceOf(DataSource.class, result); + } + + @ParameterizedTest(name = "Exception thrown when the DB type [{arguments}] is not supported") + @ValueSource(strings = {"oracle", "mysql", "mongoDb"}) + void exceptionThrownWhenDBTypeIsUnsupported(String datasourceType) { + var system = mock(ApplicationProperties.System.class); + var datasource = mock(ApplicationProperties.Datasource.class); + + when(applicationProperties.getSystem()).thenReturn(system); + when(system.getDatasource()).thenReturn(datasource); + when(datasource.isEnableCustomDatabase()).thenReturn(false); + when(datasource.getType()).thenReturn(datasourceType); + + assertThrows(UnsupportedProviderException.class, () -> databaseConfig.dataSource()); + } +} \ No newline at end of file diff --git a/src/test/java/stirling/software/SPDF/integrationtests/SPDFApplicationIntegrationTest.java b/src/test/java/stirling/software/SPDF/integrationtests/SPDFApplicationIntegrationTest.java new file mode 100644 index 00000000..199b6dd3 --- /dev/null +++ b/src/test/java/stirling/software/SPDF/integrationtests/SPDFApplicationIntegrationTest.java @@ -0,0 +1,59 @@ +package stirling.software.SPDF.integrationtests; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import stirling.software.SPDF.SPDFApplication; +import static java.nio.file.Files.createDirectories; +import static java.nio.file.Files.createFile; +import static java.nio.file.Files.delete; +import static java.nio.file.Files.exists; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest +public class SPDFApplicationIntegrationTest { + + @Test + public void testMainApplicationStartup() throws IOException, InterruptedException { + // Setup mock environment for the main method + Path configPath = Path.of("test/configs"); + Path settingsPath = Paths.get("test/configs/settings.yml"); + Path customSettingsPath = Paths.get("test/configs/custom_settings.yml"); + Path staticPath = Path.of("test/customFiles/static/"); + Path templatesPath = Path.of("test/customFiles/templates/"); + + // Ensure the files do not exist for the test + if (exists(settingsPath)) { + delete(settingsPath); + } + if (exists(customSettingsPath)) { + delete(customSettingsPath); + } + if (exists(staticPath)) { + delete(staticPath); + } + if (exists(templatesPath)) { + delete(templatesPath); + } + + // Ensure the directories are created for testing + createDirectories(configPath); + createDirectories(staticPath); + createDirectories(templatesPath); + + createFile(settingsPath); + createFile(customSettingsPath); + + // Run the main method + SPDFApplication.main(new String[] {"-Dspring.profiles.active=default"}); + + // Verify that the directories were created + assertTrue(exists(settingsPath)); + assertTrue(exists(customSettingsPath)); + assertTrue(exists(staticPath)); + assertTrue(exists(templatesPath)); + } +}