mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-25 21:59:23 +00:00
feat(database,Jwt): relocate backups and Jwt-keys to config/backup
and add Enterprise cleanup endpoints (#4225)
# Description of Changes - **What was changed** - Centralized installation paths: - Introduced `BACKUP_PATH`, `BACKUP_DB_PATH`, and `BACKUP_PRIVATE_KEY_PATH` in `InstallationPathConfig`; `getPrivateKeyPath()` now resolves to `backup/keys` and new `getBackupPath()` returns `backup/db`. - Removed old `PRIVATE_KEY_PATH` and switched all usages to the new locations. - Database service enhancements: - `DatabaseService` now uses `InstallationPathConfig.getBackupPath()` and includes a one-time migration to move existing backups from `config/db/backup` to `config/backup/db` (**@Deprecated(since = "2.0.0", forRemoval = true)**). - Added `deleteAllBackups()` and `deleteLastBackup()` methods and exposed them via a new Enterprise controller. - New Enterprise-only API: - Added `DatabaseControllerEnterprise` with: - `DELETE /api/v1/database/deleteAll` — delete all backup files. - `DELETE /api/v1/database/deleteLast` — delete the most recent backup. - Endpoints gated by `@EnterpriseEndpoint` and `@Conditional(H2SQLCondition.class)`. - Key persistence adjustments: - `KeyPersistenceService` now migrates keys from `config/db/keys` to `config/backup/keys` on startup (**@Deprecated(since = "2.0.0", forRemoval = true)**). - Miscellaneous refactors/fixes: - Switched driver resolution in `DatabaseConfig` to a switch expression. - Corrected HTTP status usage to `HttpStatus.SEE_OTHER`. - Removed constructor `runningEE` flag from `AccountWebController` and replaced EE checks with `@EnterpriseEndpoint`. - Minor test and annotation improvements (e.g., `@Deprecated(since = "0.45.0")`, method references, equals order). - **Why the change was made** - To standardize and future-proof storage locations for both backups and keys under a clear `config/backup` hierarchy. - To give Enterprise admins first-class, safe cleanup endpoints for managing backup retention without manual file operations. - To reduce conditional logic in controllers and rely on declarative EE gating. - To improve maintainability and correctness (status codes, switch expression, null-safety patterns). --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
40cf337b23
commit
3af93f0adb
@ -14,18 +14,22 @@ public class InstallationPathConfig {
|
|||||||
private static final String CONFIG_PATH;
|
private static final String CONFIG_PATH;
|
||||||
private static final String CUSTOM_FILES_PATH;
|
private static final String CUSTOM_FILES_PATH;
|
||||||
private static final String CLIENT_WEBUI_PATH;
|
private static final String CLIENT_WEBUI_PATH;
|
||||||
private static final String SCRIPTS_PATH;
|
|
||||||
private static final String PIPELINE_PATH;
|
private static final String PIPELINE_PATH;
|
||||||
|
|
||||||
// Config paths
|
// Config paths
|
||||||
private static final String SETTINGS_PATH;
|
private static final String SETTINGS_PATH;
|
||||||
private static final String CUSTOM_SETTINGS_PATH;
|
private static final String CUSTOM_SETTINGS_PATH;
|
||||||
|
private static final String SCRIPTS_PATH;
|
||||||
|
private static final String BACKUP_PATH;
|
||||||
|
|
||||||
|
// Backup paths
|
||||||
|
private static final String BACKUP_DB_PATH;
|
||||||
|
private static final String BACKUP_PRIVATE_KEY_PATH;
|
||||||
|
|
||||||
// Custom file paths
|
// Custom file paths
|
||||||
private static final String STATIC_PATH;
|
private static final String STATIC_PATH;
|
||||||
private static final String TEMPLATES_PATH;
|
private static final String TEMPLATES_PATH;
|
||||||
private static final String SIGNATURES_PATH;
|
private static final String SIGNATURES_PATH;
|
||||||
private static final String PRIVATE_KEY_PATH;
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
BASE_PATH = initializeBasePath();
|
BASE_PATH = initializeBasePath();
|
||||||
@ -41,12 +45,16 @@ public class InstallationPathConfig {
|
|||||||
SETTINGS_PATH = CONFIG_PATH + "settings.yml";
|
SETTINGS_PATH = CONFIG_PATH + "settings.yml";
|
||||||
CUSTOM_SETTINGS_PATH = CONFIG_PATH + "custom_settings.yml";
|
CUSTOM_SETTINGS_PATH = CONFIG_PATH + "custom_settings.yml";
|
||||||
SCRIPTS_PATH = CONFIG_PATH + "scripts" + File.separator;
|
SCRIPTS_PATH = CONFIG_PATH + "scripts" + File.separator;
|
||||||
|
BACKUP_PATH = CONFIG_PATH + "backup" + File.separator;
|
||||||
|
|
||||||
|
// Initialize backup paths
|
||||||
|
BACKUP_DB_PATH = BACKUP_PATH + "db" + File.separator;
|
||||||
|
BACKUP_PRIVATE_KEY_PATH = BACKUP_PATH + "keys" + File.separator;
|
||||||
|
|
||||||
// Initialize custom file paths
|
// Initialize custom file paths
|
||||||
STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator;
|
STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator;
|
||||||
TEMPLATES_PATH = CUSTOM_FILES_PATH + "templates" + File.separator;
|
TEMPLATES_PATH = CUSTOM_FILES_PATH + "templates" + File.separator;
|
||||||
SIGNATURES_PATH = CUSTOM_FILES_PATH + "signatures" + File.separator;
|
SIGNATURES_PATH = CUSTOM_FILES_PATH + "signatures" + File.separator;
|
||||||
PRIVATE_KEY_PATH = CONFIG_PATH + "db" + File.separator + "keys" + File.separator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String initializeBasePath() {
|
private static String initializeBasePath() {
|
||||||
@ -124,6 +132,10 @@ public class InstallationPathConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String getPrivateKeyPath() {
|
public static String getPrivateKeyPath() {
|
||||||
return PRIVATE_KEY_PATH;
|
return BACKUP_PRIVATE_KEY_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getBackupPath() {
|
||||||
|
return BACKUP_DB_PATH;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,7 +179,7 @@ class ApplicationPropertiesLogicTest {
|
|||||||
assertEquals(30, t.getOcrMyPdfTimeoutMinutes());
|
assertEquals(30, t.getOcrMyPdfTimeoutMinutes());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated
|
@Deprecated(since = "0.45.0")
|
||||||
@Test
|
@Test
|
||||||
void enterprise_metadata_defaults() {
|
void enterprise_metadata_defaults() {
|
||||||
ApplicationProperties.EnterpriseEdition ee = new ApplicationProperties.EnterpriseEdition();
|
ApplicationProperties.EnterpriseEdition ee = new ApplicationProperties.EnterpriseEdition();
|
||||||
|
@ -11,7 +11,6 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
@ -59,19 +58,16 @@ public class AccountWebController {
|
|||||||
private final SessionPersistentRegistry sessionPersistentRegistry;
|
private final SessionPersistentRegistry sessionPersistentRegistry;
|
||||||
// Assuming you have a repository for user operations
|
// Assuming you have a repository for user operations
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final boolean runningEE;
|
|
||||||
private final TeamRepository teamRepository;
|
private final TeamRepository teamRepository;
|
||||||
|
|
||||||
public AccountWebController(
|
public AccountWebController(
|
||||||
ApplicationProperties applicationProperties,
|
ApplicationProperties applicationProperties,
|
||||||
SessionPersistentRegistry sessionPersistentRegistry,
|
SessionPersistentRegistry sessionPersistentRegistry,
|
||||||
UserRepository userRepository,
|
UserRepository userRepository,
|
||||||
TeamRepository teamRepository,
|
TeamRepository teamRepository) {
|
||||||
@Qualifier("runningEE") boolean runningEE) {
|
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.runningEE = runningEE;
|
|
||||||
this.teamRepository = teamRepository;
|
this.teamRepository = teamRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,11 +203,9 @@ public class AccountWebController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
|
@EnterpriseEndpoint
|
||||||
@GetMapping("/usage")
|
@GetMapping("/usage")
|
||||||
public String showUsage() {
|
public String showUsage() {
|
||||||
if (!runningEE) {
|
|
||||||
return "error";
|
|
||||||
}
|
|
||||||
return "usage";
|
return "usage";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,7 +237,7 @@ public class AccountWebController {
|
|||||||
|
|
||||||
// Also check if user is part of the Internal team
|
// Also check if user is part of the Internal team
|
||||||
if (user.getTeam() != null
|
if (user.getTeam() != null
|
||||||
&& user.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
&& TeamService.INTERNAL_TEAM_NAME.equals(user.getTeam().getName())) {
|
||||||
shouldRemove = true;
|
shouldRemove = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -362,11 +356,9 @@ public class AccountWebController {
|
|||||||
teamRepository.findAll().stream()
|
teamRepository.findAll().stream()
|
||||||
.filter(
|
.filter(
|
||||||
team ->
|
team ->
|
||||||
!team.getName()
|
!stirling.software.proprietary.security.service.TeamService
|
||||||
.equals(
|
.INTERNAL_TEAM_NAME
|
||||||
stirling.software.proprietary.security
|
.equals(team.getName()))
|
||||||
.service.TeamService
|
|
||||||
.INTERNAL_TEAM_NAME))
|
|
||||||
.toList();
|
.toList();
|
||||||
model.addAttribute("teams", allTeams);
|
model.addAttribute("teams", allTeams);
|
||||||
|
|
||||||
|
@ -134,21 +134,21 @@ public class DatabaseConfig {
|
|||||||
ApplicationProperties.Driver driver =
|
ApplicationProperties.Driver driver =
|
||||||
ApplicationProperties.Driver.valueOf(driverName.toUpperCase());
|
ApplicationProperties.Driver.valueOf(driverName.toUpperCase());
|
||||||
|
|
||||||
switch (driver) {
|
return switch (driver) {
|
||||||
case H2 -> {
|
case H2 -> {
|
||||||
log.debug("H2 driver selected");
|
log.debug("H2 driver selected");
|
||||||
return DatabaseDriver.H2.getDriverClassName();
|
yield DatabaseDriver.H2.getDriverClassName();
|
||||||
}
|
}
|
||||||
case POSTGRESQL -> {
|
case POSTGRESQL -> {
|
||||||
log.debug("Postgres driver selected");
|
log.debug("Postgres driver selected");
|
||||||
return DatabaseDriver.POSTGRESQL.getDriverClassName();
|
yield DatabaseDriver.POSTGRESQL.getDriverClassName();
|
||||||
}
|
}
|
||||||
default -> {
|
default -> {
|
||||||
log.warn("{} driver selected", driverName);
|
log.warn("{} driver selected", driverName);
|
||||||
throw new UnsupportedProviderException(
|
throw new UnsupportedProviderException(
|
||||||
driverName + " is not currently supported");
|
driverName + " is not currently supported");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
log.warn("Unknown driver: {}", driverName);
|
log.warn("Unknown driver: {}", driverName);
|
||||||
throw new UnsupportedProviderException(driverName + " is not currently supported");
|
throw new UnsupportedProviderException(driverName + " is not currently supported");
|
||||||
|
@ -7,10 +7,10 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
|
|
||||||
import org.eclipse.jetty.http.HttpStatus;
|
|
||||||
import org.springframework.context.annotation.Conditional;
|
import org.springframework.context.annotation.Conditional;
|
||||||
import org.springframework.core.io.InputStreamResource;
|
import org.springframework.core.io.InputStreamResource;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
@ -145,7 +145,7 @@ public class DatabaseController {
|
|||||||
.body(resource);
|
.body(resource);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("Error downloading file: {}", e.getMessage());
|
log.error("Error downloading file: {}", e.getMessage());
|
||||||
return ResponseEntity.status(HttpStatus.SEE_OTHER_303)
|
return ResponseEntity.status(HttpStatus.SEE_OTHER)
|
||||||
.location(URI.create("/database?error=downloadFailed"))
|
.location(URI.create("/database?error=downloadFailed"))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,101 @@
|
|||||||
|
package stirling.software.proprietary.security.controller.api.enterprise;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
import org.springframework.context.annotation.Conditional;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.model.FileInfo;
|
||||||
|
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||||
|
import stirling.software.proprietary.security.database.H2SQLCondition;
|
||||||
|
import stirling.software.proprietary.security.service.DatabaseService;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/api/v1/database")
|
||||||
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
|
@EnterpriseEndpoint
|
||||||
|
@Conditional(H2SQLCondition.class)
|
||||||
|
@Tag(name = "Database", description = "Database APIs for backup, import, and management")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DatabaseControllerEnterprise {
|
||||||
|
|
||||||
|
private final DatabaseService databaseService;
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Delete the last database backup file",
|
||||||
|
description =
|
||||||
|
"Only Enterprise - Deletes the last database backup file from the server.")
|
||||||
|
@DeleteMapping("/deleteLast")
|
||||||
|
public ResponseEntity<?> deleteLastFile() {
|
||||||
|
log.info("Deleting last database backup file...");
|
||||||
|
List<Pair<FileInfo, Boolean>> results = databaseService.deleteLastBackup();
|
||||||
|
return getDeleteAllResults(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Delete all database backup files",
|
||||||
|
description = "Only Enterprise - Deletes all database backup files from the server.")
|
||||||
|
@DeleteMapping("/deleteAll")
|
||||||
|
public ResponseEntity<?> deleteAllFiles() {
|
||||||
|
log.info("Deleting all database backup files...");
|
||||||
|
List<Pair<FileInfo, Boolean>> results = databaseService.deleteAllBackups();
|
||||||
|
return getDeleteAllResults(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<?> getDeleteAllResults(List<Pair<FileInfo, Boolean>> results) {
|
||||||
|
if (results.isEmpty()) {
|
||||||
|
log.info("No backup files found to delete.");
|
||||||
|
return ResponseEntity.ok(new DeleteAllResult(List.of(), List.of(), "noContent"));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> deleted =
|
||||||
|
results.stream()
|
||||||
|
.filter(p -> Boolean.TRUE.equals(p.getRight()))
|
||||||
|
.map(p -> p.getLeft().getFileName())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
List<String> failed =
|
||||||
|
results.stream()
|
||||||
|
.filter(p -> !Boolean.TRUE.equals(p.getRight()))
|
||||||
|
.map(p -> p.getLeft().getFileName())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
log.info("Deleted backup files: {}", deleted);
|
||||||
|
if (!failed.isEmpty()) {
|
||||||
|
log.warn("Some backup files could not be deleted: {}", failed);
|
||||||
|
return ResponseEntity.status(HttpStatus.MULTI_STATUS) // 207
|
||||||
|
.body(new DeleteAllResult(deleted, failed, "partialFailure"));
|
||||||
|
}
|
||||||
|
DeleteAllResult result = new DeleteAllResult(deleted, failed, "ok");
|
||||||
|
log.debug(
|
||||||
|
"DeleteAllResult: deleted={}, failed={}, status={}",
|
||||||
|
result.deleted,
|
||||||
|
result.failed,
|
||||||
|
result.status);
|
||||||
|
return ResponseEntity.ok(result); // 200
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class DeleteAllResult {
|
||||||
|
public final List<String> deleted;
|
||||||
|
public final List<String> failed;
|
||||||
|
public final String status;
|
||||||
|
|
||||||
|
public DeleteAllResult(List<String> deleted, List<String> failed, String status) {
|
||||||
|
this.deleted = deleted;
|
||||||
|
this.failed = failed;
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ import java.nio.file.DirectoryStream;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
@ -21,6 +22,7 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
import org.springframework.jdbc.datasource.init.CannotReadScriptException;
|
import org.springframework.jdbc.datasource.init.CannotReadScriptException;
|
||||||
import org.springframework.jdbc.datasource.init.ScriptException;
|
import org.springframework.jdbc.datasource.init.ScriptException;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -45,10 +47,39 @@ public class DatabaseService implements DatabaseServiceInterface {
|
|||||||
|
|
||||||
public DatabaseService(
|
public DatabaseService(
|
||||||
ApplicationProperties.Datasource datasourceProps, DataSource dataSource) {
|
ApplicationProperties.Datasource datasourceProps, DataSource dataSource) {
|
||||||
this.BACKUP_DIR =
|
this.BACKUP_DIR = Paths.get(InstallationPathConfig.getBackupPath()).normalize();
|
||||||
Paths.get(InstallationPathConfig.getConfigPath(), "db", "backup").normalize();
|
|
||||||
this.datasourceProps = datasourceProps;
|
this.datasourceProps = datasourceProps;
|
||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
|
moveBackupFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Move all backup files from db/backup to backup/db */
|
||||||
|
@Deprecated(since = "2.0.0", forRemoval = true)
|
||||||
|
private void moveBackupFiles() {
|
||||||
|
Path sourceDir =
|
||||||
|
Paths.get(InstallationPathConfig.getConfigPath(), "db", "backup").normalize();
|
||||||
|
|
||||||
|
if (!Files.exists(sourceDir)) {
|
||||||
|
log.info("Source directory does not exist: {}", sourceDir);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Files.createDirectories(BACKUP_DIR);
|
||||||
|
try (DirectoryStream<Path> stream = Files.newDirectoryStream(sourceDir)) {
|
||||||
|
for (Path entry : stream) {
|
||||||
|
if (entry.getFileName().toString().startsWith(BACKUP_PREFIX)
|
||||||
|
&& entry.getFileName().toString().endsWith(SQL_SUFFIX)) {
|
||||||
|
Files.move(
|
||||||
|
entry,
|
||||||
|
BACKUP_DIR.resolve(entry.getFileName()),
|
||||||
|
StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error moving backup files: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -198,6 +229,46 @@ public class DatabaseService implements DatabaseServiceInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Pair<FileInfo, Boolean>> deleteAllBackups() {
|
||||||
|
List<FileInfo> backupList = this.getBackupList();
|
||||||
|
List<Pair<FileInfo, Boolean>> deletedFiles = new ArrayList<>();
|
||||||
|
|
||||||
|
for (FileInfo backup : backupList) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(Paths.get(backup.getFilePath()));
|
||||||
|
deletedFiles.add(Pair.of(backup, true));
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error deleting backup file: {}", backup.getFileName(), e);
|
||||||
|
deletedFiles.add(Pair.of(backup, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deletedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Pair<FileInfo, Boolean>> deleteLastBackup() {
|
||||||
|
|
||||||
|
List<FileInfo> backupList = this.getBackupList();
|
||||||
|
List<Pair<FileInfo, Boolean>> deletedFiles = new ArrayList<>();
|
||||||
|
if (!backupList.isEmpty()) {
|
||||||
|
FileInfo lastBackup = backupList.get(backupList.size() - 1);
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(Paths.get(lastBackup.getFilePath()));
|
||||||
|
deletedFiles.add(Pair.of(lastBackup, true));
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error deleting last backup file: {}", lastBackup.getFileName(), e);
|
||||||
|
deletedFiles.add(Pair.of(lastBackup, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deletedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the oldest backup file from the specified list.
|
||||||
|
*
|
||||||
|
* @param filteredBackupList the list of backup files
|
||||||
|
*/
|
||||||
private static void deleteOldestBackup(List<FileInfo> filteredBackupList) {
|
private static void deleteOldestBackup(List<FileInfo> filteredBackupList) {
|
||||||
try {
|
try {
|
||||||
filteredBackupList.sort(
|
filteredBackupList.sort(
|
||||||
@ -237,6 +308,11 @@ public class DatabaseService implements DatabaseServiceInterface {
|
|||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Checks if the current datasource is H2.
|
||||||
|
*
|
||||||
|
* @return true if the datasource is H2, false otherwise
|
||||||
|
*/
|
||||||
private boolean isH2Database() {
|
private boolean isH2Database() {
|
||||||
boolean isTypeH2 =
|
boolean isTypeH2 =
|
||||||
datasourceProps.getType().equalsIgnoreCase(ApplicationProperties.Driver.H2.name());
|
datasourceProps.getType().equalsIgnoreCase(ApplicationProperties.Driver.H2.name());
|
||||||
@ -301,6 +377,11 @@ public class DatabaseService implements DatabaseServiceInterface {
|
|||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a database script.
|
||||||
|
*
|
||||||
|
* @param scriptPath the path to the script file
|
||||||
|
*/
|
||||||
private void executeDatabaseScript(Path scriptPath) {
|
private void executeDatabaseScript(Path scriptPath) {
|
||||||
if (isH2Database()) {
|
if (isH2Database()) {
|
||||||
String query = "RUNSCRIPT from ?;";
|
String query = "RUNSCRIPT from ?;";
|
||||||
|
@ -3,6 +3,8 @@ package stirling.software.proprietary.security.service;
|
|||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
|
||||||
import stirling.software.common.model.FileInfo;
|
import stirling.software.common.model.FileInfo;
|
||||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||||
|
|
||||||
@ -14,4 +16,8 @@ public interface DatabaseServiceInterface {
|
|||||||
boolean hasBackup();
|
boolean hasBackup();
|
||||||
|
|
||||||
List<FileInfo> getBackupList();
|
List<FileInfo> getBackupList();
|
||||||
|
|
||||||
|
List<Pair<FileInfo, Boolean>> deleteAllBackups();
|
||||||
|
|
||||||
|
List<Pair<FileInfo, Boolean>> deleteLastBackup();
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package stirling.software.proprietary.security.service;
|
package stirling.software.proprietary.security.service;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.DirectoryStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.security.KeyFactory;
|
import java.security.KeyFactory;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.KeyPairGenerator;
|
import java.security.KeyPairGenerator;
|
||||||
@ -52,6 +54,34 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
|
|||||||
this.verifyingKeyCache = cacheManager.getCache("verifyingKeys");
|
this.verifyingKeyCache = cacheManager.getCache("verifyingKeys");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Move all key files from db/keys to backup/keys */
|
||||||
|
@Deprecated(since = "2.0.0", forRemoval = true)
|
||||||
|
private void moveKeysToBackup() {
|
||||||
|
Path sourceDir =
|
||||||
|
Paths.get(InstallationPathConfig.getConfigPath(), "db", "keys").normalize();
|
||||||
|
|
||||||
|
if (!Files.exists(sourceDir)) {
|
||||||
|
log.info("Source directory does not exist: {}", sourceDir);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path targetDir = Paths.get(InstallationPathConfig.getPrivateKeyPath()).normalize();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Files.createDirectories(targetDir);
|
||||||
|
try (DirectoryStream<Path> stream = Files.newDirectoryStream(sourceDir)) {
|
||||||
|
for (Path entry : stream) {
|
||||||
|
Files.move(
|
||||||
|
entry,
|
||||||
|
targetDir.resolve(entry.getFileName()),
|
||||||
|
StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error moving key files to backup: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void initializeKeystore() {
|
public void initializeKeystore() {
|
||||||
if (!isKeystoreEnabled()) {
|
if (!isKeystoreEnabled()) {
|
||||||
@ -59,6 +89,7 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
moveKeysToBackup();
|
||||||
ensurePrivateKeyDirectoryExists();
|
ensurePrivateKeyDirectoryExists();
|
||||||
loadKeyPair();
|
loadKeyPair();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
@ -2,6 +2,7 @@ package stirling.software.proprietary.service;
|
|||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.boot.actuate.audit.AuditEvent;
|
import org.springframework.boot.actuate.audit.AuditEvent;
|
||||||
import org.springframework.boot.actuate.audit.AuditEventRepository;
|
import org.springframework.boot.actuate.audit.AuditEventRepository;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
@ -29,8 +30,7 @@ public class AuditService {
|
|||||||
public AuditService(
|
public AuditService(
|
||||||
AuditEventRepository repository,
|
AuditEventRepository repository,
|
||||||
AuditConfigurationProperties auditConfig,
|
AuditConfigurationProperties auditConfig,
|
||||||
@org.springframework.beans.factory.annotation.Qualifier("runningEE")
|
@Qualifier("runningEE") boolean runningEE) {
|
||||||
boolean runningEE) {
|
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
this.auditConfig = auditConfig;
|
this.auditConfig = auditConfig;
|
||||||
this.runningEE = runningEE;
|
this.runningEE = runningEE;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user