2270: clean up

This commit is contained in:
DarioGii 2024-12-22 12:26:54 +00:00 committed by Dario Ghunney Ware
parent de068f0458
commit fb48e0d67c
5 changed files with 231 additions and 138 deletions

View File

@ -0,0 +1,17 @@
package stirling.software.SPDF.config.interfaces;
import java.sql.SQLException;
import java.util.List;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.utils.FileInfo;
public interface DatabaseInterface {
void exportDatabase() throws SQLException, UnsupportedProviderException;
void importDatabase();
boolean hasBackup();
List<FileInfo> getBackupList();
}

View File

@ -1,49 +1,48 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.sql.SQLException;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
@Component
@Slf4j
@Component
public class InitialSecuritySetup {
private final UserService userService;
@Autowired private UserService userService;
private final ApplicationProperties applicationProperties;
@Autowired private ApplicationProperties applicationProperties;
private final DatabaseBackupInterface databaseBackupHelper;
public InitialSecuritySetup(
UserService userService,
ApplicationProperties applicationProperties,
DatabaseBackupInterface databaseBackupHelper) {
this.userService = userService;
this.applicationProperties = applicationProperties;
this.databaseBackupHelper = databaseBackupHelper;
}
@Autowired private DatabaseInterface databaseService;
@PostConstruct
public void init() throws IllegalArgumentException, IOException {
if (databaseBackupHelper.hasBackup() && !userService.hasUsers()) {
databaseBackupHelper.importDatabase();
} else if (!userService.hasUsers()) {
initializeAdminUser();
} else {
databaseBackupHelper.exportDatabase();
public void init() {
try {
if (databaseService.hasBackup()) {
databaseService.importDatabase();
}
if (!userService.hasUsers()) {
initializeAdminUser();
}
userService.migrateOauth2ToSSO();
initializeInternalApiUser();
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
log.error("Failed to initialize security setup.", e);
System.exit(1);
}
initializeInternalApiUser();
}
private void initializeAdminUser() throws IOException {
private void initializeAdminUser() throws SQLException, UnsupportedProviderException {
String initialUsername =
applicationProperties.getSecurity().getInitialLogin().getUsername();
String initialPassword =
@ -52,36 +51,34 @@ public class InitialSecuritySetup {
&& !initialUsername.isEmpty()
&& initialPassword != null
&& !initialPassword.isEmpty()
&& !userService.findByUsernameIgnoreCase(initialUsername).isPresent()) {
try {
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
log.info("Admin user created: " + initialUsername);
} catch (IllegalArgumentException e) {
log.error("Failed to initialize security setup", e);
System.exit(1);
}
&& userService.findByUsernameIgnoreCase(initialUsername).isEmpty()) {
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
log.info("Admin user created: {}", initialUsername);
} else {
createDefaultAdminUser();
}
}
private void createDefaultAdminUser() throws IllegalArgumentException, IOException {
private void createDefaultAdminUser() throws SQLException, UnsupportedProviderException {
String defaultUsername = "admin";
String defaultPassword = "stirling";
if (!userService.findByUsernameIgnoreCase(defaultUsername).isPresent()) {
if (userService.findByUsernameIgnoreCase(defaultUsername).isEmpty()) {
userService.saveUser(defaultUsername, defaultPassword, Role.ADMIN.getRoleId(), true);
log.info("Default admin user created: " + defaultUsername);
log.info("Default admin user created: {}", defaultUsername);
}
}
private void initializeInternalApiUser() throws IllegalArgumentException, IOException {
private void initializeInternalApiUser()
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!userService.usernameExistsIgnoreCase(Role.INTERNAL_API_USER.getRoleId())) {
userService.saveUser(
Role.INTERNAL_API_USER.getRoleId(),
UUID.randomUUID().toString(),
Role.INTERNAL_API_USER.getRoleId());
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId());
log.info("Internal API user created: {}", Role.INTERNAL_API_USER.getRoleId());
}
userService.syncCustomApiUser(applicationProperties.getSecurity().getCustomGlobalAPIKey());
}

View File

@ -6,7 +6,10 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.sql.*;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
@ -15,49 +18,58 @@ import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.PathResource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.jdbc.datasource.init.ScriptException;
import org.springframework.jdbc.datasource.init.ScriptUtils;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
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
@Configuration
public class DatabaseBackupHelper implements DatabaseBackupInterface {
@Service
public class DatabaseService implements DatabaseInterface {
@Value("${spring.datasource.url}")
private String url;
public static final String BACKUP_PREFIX = "backup_";
public static final String SQL_SUFFIX = ".sql";
private static final String BACKUP_DIR = "configs/db/backup/";
@Value("${spring.datasource.username}")
private String databaseUsername;
@Value("${spring.datasource.password}")
private String databasePassword;
private Path backupPath = Paths.get("configs/db/backup/");
@Autowired private DatabaseConfig databaseConfig;
/**
* Checks if there is at least one backup
*
* @return true if there are backup scripts, false if there are not
*/
@Override
public boolean hasBackup() {
// Check if there is at least one backup
return !getBackupList().isEmpty();
Path filePath = Paths.get(BACKUP_DIR + "*");
return Files.exists(filePath);
}
/**
* Read the backup directory and filter for files with the prefix "backup_" and suffix ".sql"
*
* @return a <code>List</code> of backup files
*/
@Override
public List<FileInfo> getBackupList() {
// Check if the backup directory exists, and create it if it does not
ensureBackupDirectoryExists();
List<FileInfo> backupFiles = new ArrayList<>();
Path backupPath = Paths.get(BACKUP_DIR);
// Read the backup directory and filter for files with the prefix "backup_" and suffix
// ".sql"
try (DirectoryStream<Path> stream =
Files.newDirectoryStream(
backupPath,
path ->
path.getFileName().toString().startsWith("backup_")
&& path.getFileName().toString().endsWith(".sql"))) {
path.getFileName().toString().startsWith(BACKUP_PREFIX)
&& path.getFileName().toString().endsWith(SQL_SUFFIX))) {
for (Path entry : stream) {
BasicFileAttributes attrs = Files.readAttributes(entry, BasicFileAttributes.class);
LocalDateTime modificationDate =
@ -78,94 +90,127 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
} catch (IOException e) {
log.error("Error reading backup directory: {}", e.getMessage(), e);
}
return backupFiles;
}
// Imports a database backup from the specified file.
public boolean importDatabaseFromUI(String fileName) throws IOException {
return this.importDatabaseFromUI(getBackupFilePath(fileName));
}
// Imports a database backup from the specified path.
public boolean importDatabaseFromUI(Path tempTemplatePath) throws IOException {
boolean success = executeDatabaseScript(tempTemplatePath);
if (success) {
LocalDateTime dateNow = LocalDateTime.now();
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
Path insertOutputFilePath =
this.getBackupFilePath("backup_user_" + dateNow.format(myFormatObj) + ".sql");
Files.copy(tempTemplatePath, insertOutputFilePath);
Files.deleteIfExists(tempTemplatePath);
}
return success;
}
@Override
public boolean importDatabase() {
if (!this.hasBackup()) return false;
public void importDatabase() {
if (!hasBackup()) throw new BackupNotFoundException("No backup scripts were found.");
List<FileInfo> backupList = this.getBackupList();
backupList.sort(Comparator.comparing(FileInfo::getModificationDate).reversed());
return executeDatabaseScript(Paths.get(backupList.get(0).getFilePath()));
executeDatabaseScript(Paths.get(backupList.get(0).getFilePath()));
}
// fixMe: Needs to check the type of DB before executing script
@Override
public void exportDatabase() throws IOException {
// Check if the backup directory exists, and create it if it does not
ensureBackupDirectoryExists();
/** Imports a database backup from the specified file. */
public boolean importDatabaseFromUI(String fileName) {
try {
importDatabaseFromUI(getBackupFilePath(fileName));
return true;
} catch (IOException e) {
log.error(
"Error importing database from file: {}, message: {}",
fileName,
e.getMessage(),
e.getCause());
return false;
}
}
// Filter and delete old backups if there are more than 5
/** Imports a database backup from the specified path. */
private void importDatabaseFromUI(Path tempTemplatePath) throws IOException {
executeDatabaseScript(tempTemplatePath);
LocalDateTime dateNow = LocalDateTime.now();
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
Path insertOutputFilePath =
this.getBackupFilePath(
BACKUP_PREFIX + "user_" + dateNow.format(myFormatObj) + SQL_SUFFIX);
Files.copy(tempTemplatePath, insertOutputFilePath);
Files.deleteIfExists(tempTemplatePath);
}
/** Filter and delete old backups if there are more than 5 */
@Override
public void exportDatabase() throws SQLException, UnsupportedProviderException {
List<FileInfo> filteredBackupList =
this.getBackupList().stream()
.filter(backup -> !backup.getFileName().startsWith("backup_user_"))
.filter(backup -> !backup.getFileName().startsWith(BACKUP_PREFIX + "user_"))
.collect(Collectors.toList());
if (filteredBackupList.size() > 5) {
filteredBackupList.sort(
Comparator.comparing(
p -> p.getFileName().substring(7, p.getFileName().length() - 4)));
Files.deleteIfExists(Paths.get(filteredBackupList.get(0).getFilePath()));
log.info("Deleted oldest backup: {}", filteredBackupList.get(0).getFileName());
deleteOldestBackup(filteredBackupList);
}
LocalDateTime dateNow = LocalDateTime.now();
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
Path insertOutputFilePath =
this.getBackupFilePath("backup_" + dateNow.format(myFormatObj) + ".sql");
String query = "SCRIPT SIMPLE COLUMNS DROP to ?;";
this.getBackupFilePath(BACKUP_PREFIX + dateNow.format(myFormatObj) + SQL_SUFFIX);
try (Connection conn = databaseConfig.connection()) {
ScriptUtils.executeSqlScript(
conn, new EncodedResource(new PathResource(insertOutputFilePath)));
try (Connection conn =
DriverManager.getConnection(url, databaseUsername, databasePassword);
PreparedStatement stmt = conn.prepareStatement(query)) {
stmt.setString(1, insertOutputFilePath.toString());
stmt.execute();
log.info("Database export completed: {}", insertOutputFilePath);
} catch (SQLException e) {
} catch (SQLException | UnsupportedProviderException e) {
log.error("Error during database export: {}", e.getMessage(), e);
throw e;
} catch (ScriptException e) {
log.error("Error during database export: File {} not found", insertOutputFilePath);
throw e;
}
}
// Retrieves the H2 database version.
private static void deleteOldestBackup(List<FileInfo> filteredBackupList) {
try {
filteredBackupList.sort(
Comparator.comparing(
p -> p.getFileName().substring(7, p.getFileName().length() - 4)));
FileInfo oldestFile = filteredBackupList.get(0);
Files.deleteIfExists(Paths.get(oldestFile.getFilePath()));
log.info("Deleted oldest backup: {}", oldestFile.getFileName());
} catch (IOException e) {
log.error("Unable to delete oldest backup, message: {}", e.getMessage(), e);
}
}
/**
* Retrieves the H2 database version.
*
* @return <code>String</code> of the H2 version
*/
public String getH2Version() {
String version = "Unknown";
try (Connection conn =
DriverManager.getConnection(url, databaseUsername, databasePassword)) {
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT H2VERSION() AS version")) {
if (rs.next()) {
version = rs.getString("version");
log.info("H2 Database Version: {}", version);
if (databaseConfig
.getApplicationProperties()
.getSystem()
.getDatasource()
.getType()
.equals(ApplicationProperties.Driver.H2.name())) {
try (Connection conn = databaseConfig.connection()) {
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT H2VERSION() AS version")) {
if (rs.next()) {
version = rs.getString("version");
log.info("H2 Database Version: {}", version);
}
}
} catch (SQLException | UnsupportedProviderException e) {
log.error("Error retrieving H2 version: {}", e.getMessage(), e);
}
} catch (SQLException e) {
log.error("Error retrieving H2 version: {}", e.getMessage(), e);
}
return version;
}
// Deletes a backup file.
/**
* Deletes a backup file.
*
* @return true if successful, false if not
*/
public boolean deleteBackupFile(String fileName) throws IOException {
if (!isValidFileName(fileName)) {
log.error("Invalid file name: {}", fileName);
@ -181,43 +226,37 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
}
}
// Gets the Path object for a given backup file name.
/**
* Gets the Path for a given backup file name.
*
* @return the <code>Path</code> object for the given file name
*/
public Path getBackupFilePath(String fileName) {
Path filePath = Paths.get(backupPath.toString(), fileName).normalize();
if (!filePath.startsWith(backupPath)) {
Path filePath = Paths.get(BACKUP_DIR, fileName).normalize();
if (!filePath.startsWith(BACKUP_DIR)) {
throw new SecurityException("Path traversal detected");
}
return filePath;
}
private boolean executeDatabaseScript(Path scriptPath) {
String query = "RUNSCRIPT from ?;";
private void executeDatabaseScript(Path scriptPath) {
try (Connection conn = databaseConfig.connection()) {
ScriptUtils.executeSqlScript(conn, new EncodedResource(new PathResource(scriptPath)));
try (Connection conn =
DriverManager.getConnection(url, databaseUsername, databasePassword);
PreparedStatement stmt = conn.prepareStatement(query)) {
stmt.setString(1, scriptPath.toString());
stmt.execute();
log.info("Database import completed: {}", scriptPath);
return true;
} catch (SQLException e) {
} catch (SQLException | UnsupportedProviderException e) {
log.error("Error during database import: {}", e.getMessage(), e);
return false;
}
}
private void ensureBackupDirectoryExists() {
if (Files.notExists(backupPath)) {
try {
Files.createDirectories(backupPath);
} catch (IOException e) {
log.error("Error creating directories: {}", e.getMessage());
}
} catch (ScriptException e) {
log.error("Error: File {} not found", scriptPath.toString(), e);
}
}
/**
* Checks for invalid characters or sequences
*
* @return true if it contains no invalid characters, false if it does
*/
private boolean isValidFileName(String fileName) {
// Check for invalid characters or sequences
return fileName != null
&& !fileName.contains("..")
&& !fileName.contains("/")

View File

@ -0,0 +1,7 @@
package stirling.software.SPDF.model.exception;
public class BackupNotFoundException extends RuntimeException {
public BackupNotFoundException(String message) {
super(message);
}
}

View File

@ -0,0 +1,33 @@
package stirling.software.SPDF.config.security.database;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class DatabaseServiceTest {
private final Path BACKUP_PATH = Paths.get("configs/db/backup/*");
@Mock
private DatabaseConfig databaseConfig;
@InjectMocks
private DatabaseService databaseService;
@Test
void testHasBackups() throws IOException {
Files.createDirectories(BACKUP_PATH);
assertTrue(databaseService.hasBackup());
}
}