merging from main

This commit is contained in:
Dario Ghunney Ware 2025-01-01 15:03:13 +00:00
parent 7d71fd0730
commit 6b9d85a119
26 changed files with 292 additions and 279 deletions

View File

@ -322,9 +322,11 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
implementation "org.springframework.session:spring-session-core:$springBootVersion"
implementation "org.springframework:spring-jdbc"
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
// Don't upgrade h2database
runtimeOnly "com.h2database:h2:2.3.232"
runtimeOnly "org.postgresql:postgresql:42.7.4"
constraints {
implementation "org.opensaml:opensaml-core:$openSamlVersion"

View File

@ -0,0 +1,63 @@
services:
stirling-pdf:
container_name: Stirling-PDF-Security-Fat-Postgres
image: stirlingtools/stirling-pdf:latest-fat
deploy:
resources:
limits:
memory: 4G
depends_on:
- db
healthcheck:
test: [ "CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'" ]
interval: 5s
timeout: 10s
retries: 16
ports:
- 8080:8080
volumes:
- ./stirling/latest/data:/usr/share/tessdata:rw
- ./stirling/latest/config:/configs:rw
- ./stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "false"
PUID: 1002
PGID: 1002
UMASK: "022"
SYSTEM_DEFAULTLOCALE: en-US
UI_APPNAME: Stirling-PDF
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest-fat with Security
UI_APPNAMENAVBAR: Stirling-PDF Latest-fat
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
SYSTEM_DATASOURCE_ENABLECUSTOMDATABASE: "true"
SYSTEM_DATASOURCE_CUSTOMDATABASEURL: "jdbc:postgresql://db:5432/stirling_pdf"
SYSTEM_DATASOURCE_USERNAME: "admin"
SYSTEM_DATASOURCE_PASSWORD: "stirling"
restart: on-failure:5
db:
image: 'postgres:17.2-alpine'
restart: on-failure:5
container_name: db
ports:
- "5432:5432"
environment:
POSTGRES_DB: "stirling_pdf"
POSTGRES_USER: "admin"
POSTGRES_PASSWORD: "stirling"
shm_size: "512mb"
deploy:
resources:
limits:
memory: 512m
cpus: "0.5"
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U admin stirling_pdf" ]
interval: 1s
timeout: 5s
retries: 10
volumes:
- ./stirling/latest/data:/pgdata

View File

@ -1,6 +1,3 @@
include:
- docker-compose-postgres.yml
services:
stirling-pdf:
container_name: Stirling-PDF-Security-Fat
@ -33,8 +30,4 @@ services:
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
SYSTEM_DATASOURCE_ENABLECUSTOMDATABASE: "true"
SYSTEM_DATASOURCE_CUSTOMDATABASEURL: "jdbc:postgresql://db:5432/stirling_pdf"
SYSTEM_DATASOURCE_USERNAME: "admin"
SYSTEM_DATASOURCE_PASSWORD: "stirling"
restart: on-failure:5

View File

@ -38,8 +38,4 @@ services:
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
SYSTEM_DATASOURCE_ENABLECUSTOMDATABASE: "true"
SYSTEM_DATASOURCE_CUSTOMDATABASEURL: "jdbc:postgresql://db:5432/stirling_pdf"
SYSTEM_DATASOURCE_USERNAME: "admin"
SYSTEM_DATASOURCE_PASSWORD: "stirling"
restart: on-failure:5

View File

@ -6,8 +6,6 @@ services:
resources:
limits:
memory: 4G
depends_on:
- db
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
interval: 5s
@ -32,8 +30,4 @@ services:
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
SYSTEM_DATASOURCE_ENABLECUSTOMDATABASE: "true"
SYSTEM_DATASOURCE_CUSTOMDATABASEURL: "jdbc:postgresql://db:5432/stirling_pdf"
SYSTEM_DATASOURCE_USERNAME: "admin"
SYSTEM_DATASOURCE_PASSWORD: "stirling"
restart: on-failure:5

View File

@ -27,8 +27,4 @@ services:
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
SYSTEM_DATASOURCE_ENABLECUSTOMDATABASE: "true"
SYSTEM_DATASOURCE_CUSTOMDATABASEURL: "jdbc:postgresql://db:5432/stirling_pdf"
SYSTEM_DATASOURCE_USERNAME: "admin"
SYSTEM_DATASOURCE_PASSWORD: "stirling"
restart: on-failure:5

View File

@ -1,18 +0,0 @@
services:
db:
image: 'postgres:17.2-alpine'
restart: on-failure:5
container_name: db
ports:
- "5432:5432"
environment:
POSTGRES_DB: "stirling_pdf"
POSTGRES_USER: "admin"
POSTGRES_PASSWORD: "stirling"
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U admin stirling_pdf" ]
interval: 1s
timeout: 5s
retries: 10
volumes:
- ./stirling/latest/data:/pgdata

View File

@ -11,6 +11,8 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
@ -28,13 +30,14 @@ import stirling.software.SPDF.config.ConfigInitializer;
import stirling.software.SPDF.config.InstallationPathConfig;
import stirling.software.SPDF.model.ApplicationProperties;
@SpringBootApplication
@EnableScheduling
@Slf4j
public class SPdfApplication {
@EnableScheduling
@SpringBootApplication
public class SPDFApplication {
private static String baseUrlStatic;
private static String serverPortStatic;
private static String baseUrlStatic;
private final Environment env;
private final ApplicationProperties applicationProperties;
private final WebBrowser webBrowser;
@ -42,7 +45,7 @@ public class SPdfApplication {
@Value("${baseUrl:http://localhost}")
private String baseUrl;
public SPdfApplication(
public SPDFApplication(
Environment env,
ApplicationProperties applicationProperties,
@Autowired(required = false) WebBrowser webBrowser) {
@ -51,33 +54,19 @@ public class SPdfApplication {
this.webBrowser = webBrowser;
}
// Optionally keep this method if you want to provide a manual port-incrementation fallback.
private static String findAvailablePort(int startPort) {
int port = startPort;
while (!isPortAvailable(port)) {
port++;
}
return String.valueOf(port);
}
private static boolean isPortAvailable(int port) {
try (ServerSocket socket = new ServerSocket(port)) {
return true;
} catch (IOException e) {
return false;
}
}
public static void main(String[] args) throws IOException, InterruptedException {
SpringApplication app = new SpringApplication(SPdfApplication.class);
SpringApplication app = new SpringApplication(SPDFApplication.class);
Properties props = new Properties();
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
System.setProperty("java.awt.headless", "false");
app.setHeadless(false);
props.put("java.awt.headless", "false");
props.put("spring.main.web-application-type", "servlet");
}
app.setAdditionalProfiles("default");
app.setAdditionalProfiles(getActiveProfile(args));
ConfigInitializer initializer = new ConfigInitializer();
try {
@ -85,8 +74,8 @@ public class SPdfApplication {
} catch (IOException | URISyntaxException e) {
log.error("Error initialising configuration", e);
}
Map<String, String> propertyFiles = new HashMap<>();
// External config files
log.info("Settings file: {}", InstallationPathConfig.getSettingsPath());
if (Files.exists(Paths.get(InstallationPathConfig.getSettingsPath()))) {
@ -98,6 +87,7 @@ public class SPdfApplication {
"External configuration file '{}' does not exist.",
InstallationPathConfig.getSettingsPath());
}
if (Files.exists(Paths.get(InstallationPathConfig.getCustomSettingsPath()))) {
String existingLocation =
propertyFiles.getOrDefault("spring.config.additional-location", "");
@ -113,17 +103,21 @@ public class SPdfApplication {
InstallationPathConfig.getCustomSettingsPath());
}
Properties finalProps = new Properties();
if (!propertyFiles.isEmpty()) {
finalProps.putAll(
Collections.singletonMap(
"spring.config.additional-location",
propertyFiles.get("spring.config.additional-location")));
}
if (!props.isEmpty()) {
finalProps.putAll(props);
}
app.setDefaultProperties(finalProps);
app.run(args);
// Ensure directories are created
try {
Files.createDirectories(Path.of(InstallationPathConfig.getTemplatesPath()));
@ -131,34 +125,10 @@ public class SPdfApplication {
} catch (Exception e) {
log.error("Error creating directories: {}", e.getMessage());
}
printStartupLogs();
}
private static void printStartupLogs() {
log.info("Stirling-PDF Started.");
String url = baseUrlStatic + ":" + getStaticPort();
log.info("Navigate to {}", url);
}
public static String getStaticBaseUrl() {
return baseUrlStatic;
}
public static String getStaticPort() {
return serverPortStatic;
}
@Value("${server.port:8080}")
public void setServerPortStatic(String port) {
if ("auto".equalsIgnoreCase(port)) {
// Use Spring Boot's automatic port assignment (server.port=0)
SPdfApplication.serverPortStatic = // This will let Spring Boot assign an available port
"0";
} else {
SPdfApplication.serverPortStatic = port;
}
}
@PostConstruct
public void init() {
baseUrlStatic = this.baseUrl;
@ -189,6 +159,17 @@ public class SPdfApplication {
log.info("Running configs {}", applicationProperties.toString());
}
@Value("${server.port:8080}")
public void setServerPortStatic(String port) {
if ("auto".equalsIgnoreCase(port)) {
// Use Spring Boot's automatic port assignment (server.port=0)
SPDFApplication.serverPortStatic =
"0"; // This will let Spring Boot assign an available port
} else {
SPDFApplication.serverPortStatic = port;
}
}
@PreDestroy
public void cleanup() {
if (webBrowser != null) {
@ -196,10 +177,55 @@ public class SPdfApplication {
}
}
private static void printStartupLogs() {
log.info("Stirling-PDF Started.");
String url = baseUrlStatic + ":" + getStaticPort();
log.info("Navigate to {}", url);
}
private static String[] getActiveProfile(String[] args) {
if (args == null) {
return new String[] {"default"};
}
for (String arg : args) {
if (arg.contains("spring.profiles.active")) {
return arg.substring(args[0].indexOf('=') + 1).split(", ");
}
}
return new String[] {"default"};
}
private static boolean isPortAvailable(int port) {
try (ServerSocket socket = new ServerSocket(port)) {
return true;
} catch (IOException e) {
return false;
}
}
// Optionally keep this method if you want to provide a manual port-incrementation fallback.
private static String findAvailablePort(int startPort) {
int port = startPort;
while (!isPortAvailable(port)) {
port++;
}
return String.valueOf(port);
}
public static String getStaticBaseUrl() {
return baseUrlStatic;
}
public String getNonStaticBaseUrl() {
return baseUrlStatic;
}
public static String getStaticPort() {
return serverPortStatic;
}
public String getNonStaticPort() {
return serverPortStatic;
}

View File

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

View File

@ -20,7 +20,7 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.SPdfApplication;
import stirling.software.SPDF.SPDFApplication;
import stirling.software.SPDF.config.security.saml2.CertificateUtils;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.model.ApplicationProperties;
@ -110,7 +110,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
// Construct URLs required for SAML configuration
String serverUrl =
SPdfApplication.getStaticBaseUrl() + ":" + SPdfApplication.getStaticPort();
SPDFApplication.getStaticBaseUrl() + ":" + SPDFApplication.getStaticPort();
String relyingPartyIdentifier =
serverUrl + "/saml2/service-provider-metadata/" + registrationId;

View File

@ -1,6 +1,7 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.sql.SQLException;
import java.util.*;
import java.util.stream.Collectors;
@ -20,11 +21,12 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.*;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.repository.AuthorityRepository;
import stirling.software.SPDF.repository.UserRepository;
@ -42,7 +44,7 @@ public class UserService implements UserServiceInterface {
private final SessionPersistentRegistry sessionRegistry;
private final DatabaseBackupInterface databaseBackupHelper;
private final DatabaseInterface databaseService;
private final ApplicationProperties applicationProperties;
@ -52,14 +54,14 @@ public class UserService implements UserServiceInterface {
PasswordEncoder passwordEncoder,
MessageSource messageSource,
SessionPersistentRegistry sessionRegistry,
DatabaseBackupInterface databaseBackupHelper,
DatabaseInterface databaseService,
ApplicationProperties applicationProperties) {
this.userRepository = userRepository;
this.authorityRepository = authorityRepository;
this.passwordEncoder = passwordEncoder;
this.messageSource = messageSource;
this.sessionRegistry = sessionRegistry;
this.databaseBackupHelper = databaseBackupHelper;
this.databaseService = databaseService;
this.applicationProperties = applicationProperties;
}
@ -76,7 +78,7 @@ public class UserService implements UserServiceInterface {
// Handle OAUTH2 login and user auto creation.
public boolean processSSOPostLogin(String username, boolean autoCreateUser)
throws IllegalArgumentException, IOException {
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!isUsernameValid(username)) {
return false;
}
@ -163,12 +165,12 @@ public class UserService implements UserServiceInterface {
}
public void saveUser(String username, AuthenticationType authenticationType)
throws IllegalArgumentException, IOException {
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
saveUser(username, authenticationType, Role.USER.getRoleId());
}
public void saveUser(String username, AuthenticationType authenticationType, String role)
throws IllegalArgumentException, IOException {
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!isUsernameValid(username)) {
throw new IllegalArgumentException(getInvalidUsernameMessage());
}
@ -179,11 +181,11 @@ public class UserService implements UserServiceInterface {
user.addAuthority(new Authority(role, user));
user.setAuthenticationType(authenticationType);
userRepository.save(user);
databaseBackupHelper.exportDatabase();
databaseService.exportDatabase();
}
public void saveUser(String username, String password)
throws IllegalArgumentException, IOException {
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!isUsernameValid(username)) {
throw new IllegalArgumentException(getInvalidUsernameMessage());
}
@ -193,11 +195,11 @@ public class UserService implements UserServiceInterface {
user.setEnabled(true);
user.setAuthenticationType(AuthenticationType.WEB);
userRepository.save(user);
databaseBackupHelper.exportDatabase();
databaseService.exportDatabase();
}
public void saveUser(String username, String password, String role, boolean firstLogin)
throws IllegalArgumentException, IOException {
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!isUsernameValid(username)) {
throw new IllegalArgumentException(getInvalidUsernameMessage());
}
@ -209,11 +211,11 @@ public class UserService implements UserServiceInterface {
user.setAuthenticationType(AuthenticationType.WEB);
user.setFirstLogin(firstLogin);
userRepository.save(user);
databaseBackupHelper.exportDatabase();
databaseService.exportDatabase();
}
public void saveUser(String username, String password, String role)
throws IllegalArgumentException, IOException {
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
saveUser(username, password, role, false);
}
@ -247,7 +249,7 @@ public class UserService implements UserServiceInterface {
}
public void updateUserSettings(String username, Map<String, String> updates)
throws IOException {
throws SQLException, UnsupportedProviderException {
Optional<User> userOpt = findByUsernameIgnoreCaseWithSettings(username);
if (userOpt.isPresent()) {
User user = userOpt.get();
@ -259,7 +261,7 @@ public class UserService implements UserServiceInterface {
settingsMap.putAll(updates);
user.setSettings(settingsMap);
userRepository.save(user);
databaseBackupHelper.exportDatabase();
databaseService.exportDatabase();
}
}
@ -280,38 +282,45 @@ public class UserService implements UserServiceInterface {
}
public void changeUsername(User user, String newUsername)
throws IllegalArgumentException, IOException {
throws IllegalArgumentException,
IOException,
SQLException,
UnsupportedProviderException {
if (!isUsernameValid(newUsername)) {
throw new IllegalArgumentException(getInvalidUsernameMessage());
}
user.setUsername(newUsername);
userRepository.save(user);
databaseBackupHelper.exportDatabase();
databaseService.exportDatabase();
}
public void changePassword(User user, String newPassword) throws IOException {
public void changePassword(User user, String newPassword)
throws SQLException, UnsupportedProviderException {
user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user);
databaseBackupHelper.exportDatabase();
databaseService.exportDatabase();
}
public void changeFirstUse(User user, boolean firstUse) throws IOException {
public void changeFirstUse(User user, boolean firstUse)
throws SQLException, UnsupportedProviderException {
user.setFirstLogin(firstUse);
userRepository.save(user);
databaseBackupHelper.exportDatabase();
databaseService.exportDatabase();
}
public void changeRole(User user, String newRole) throws IOException {
public void changeRole(User user, String newRole)
throws SQLException, UnsupportedProviderException {
Authority userAuthority = this.findRole(user);
userAuthority.setAuthority(newRole);
authorityRepository.save(userAuthority);
databaseBackupHelper.exportDatabase();
databaseService.exportDatabase();
}
public void changeUserEnabled(User user, Boolean enbeled) throws IOException {
public void changeUserEnabled(User user, Boolean enbeled)
throws SQLException, UnsupportedProviderException {
user.setEnabled(enbeled);
userRepository.save(user);
databaseBackupHelper.exportDatabase();
databaseService.exportDatabase();
}
public boolean isPasswordCorrect(User user, String currentPassword) {
@ -397,7 +406,8 @@ public class UserService implements UserServiceInterface {
}
@Transactional
public void syncCustomApiUser(String customApiKey) throws IOException {
public void syncCustomApiUser(String customApiKey)
throws SQLException, UnsupportedProviderException {
if (customApiKey == null || customApiKey.trim().length() == 0) {
return;
}
@ -414,14 +424,14 @@ public class UserService implements UserServiceInterface {
user.setApiKey(customApiKey);
user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user));
userRepository.save(user);
databaseBackupHelper.exportDatabase();
databaseService.exportDatabase();
} else {
// Update API key if it has changed
User user = existingUser.get();
if (!customApiKey.equals(user.getApiKey())) {
user.setApiKey(customApiKey);
userRepository.save(user);
databaseBackupHelper.exportDatabase();
databaseService.exportDatabase();
}
}
}

View File

@ -31,7 +31,7 @@ public class DatabaseConfig {
@Autowired
public DatabaseConfig(ApplicationProperties applicationProperties, boolean runningEE) {
this.applicationProperties = applicationProperties;
this.runningEE = runningEE;
this.runningEE = true; // fixMe: change back
}
/**

View File

@ -133,7 +133,7 @@ public class DatabaseService implements DatabaseInterface {
}
/** Imports a database backup from the specified path. */
private void importDatabaseFromUI(Path tempTemplatePath) throws IOException {
public boolean importDatabaseFromUI(Path tempTemplatePath) throws IOException {
executeDatabaseScript(tempTemplatePath);
LocalDateTime dateNow = LocalDateTime.now();
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
@ -142,6 +142,7 @@ public class DatabaseService implements DatabaseInterface {
BACKUP_PREFIX + "user_" + dateNow.format(myFormatObj) + SQL_SUFFIX);
Files.copy(tempTemplatePath, insertOutputFilePath);
Files.deleteIfExists(tempTemplatePath);
return true;
}
/** Filter and delete old backups if there are more than 5 */

View File

@ -1,21 +1,24 @@
package stirling.software.SPDF.config.security.database;
import java.io.IOException;
import java.sql.SQLException;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
@Component
public class ScheduledTasks {
private final DatabaseBackupHelper databaseBackupService;
private final DatabaseInterface databaseService;
public ScheduledTasks(DatabaseBackupHelper databaseBackupService) {
this.databaseBackupService = databaseBackupService;
public ScheduledTasks(DatabaseInterface databaseService) {
this.databaseService = databaseService;
}
@Scheduled(cron = "0 0 0 * * ?")
public void performBackup() throws IOException {
databaseBackupService.exportDatabase();
public void performBackup() throws SQLException, UnsupportedProviderException {
databaseService.exportDatabase();
}
}

View File

@ -1,6 +1,7 @@
package stirling.software.SPDF.config.security.oauth2;
import java.io.IOException;
import java.sql.SQLException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication;
@ -18,6 +19,7 @@ import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.utils.RequestUriUtils;
public class CustomOAuth2AuthenticationSuccessHandler
@ -97,10 +99,8 @@ public class CustomOAuth2AuthenticationSuccessHandler
userService.processSSOPostLogin(username, oAuth.getAutoCreateUser());
}
response.sendRedirect(contextPath + "/");
return;
} catch (IllegalArgumentException e) {
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
return;
}
}
}

View File

@ -1,6 +1,7 @@
package stirling.software.SPDF.config.security.saml2;
import java.io.IOException;
import java.sql.SQLException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication;
@ -18,6 +19,7 @@ import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.utils.RequestUriUtils;
@AllArgsConstructor
@ -109,7 +111,7 @@ public class CustomSaml2AuthenticationSuccessHandler
log.debug("Successfully processed authentication for user: {}", username);
response.sendRedirect(contextPath + "/");
return;
} catch (IllegalArgumentException e) {
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
log.debug(
"Invalid username detected for user: {}, redirecting to logout",
username);

View File

@ -24,7 +24,7 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.database.DatabaseBackupHelper;
import stirling.software.SPDF.config.security.database.DatabaseService;
@Slf4j
@Controller
@ -33,10 +33,10 @@ import stirling.software.SPDF.config.security.database.DatabaseBackupHelper;
@Tag(name = "Database", description = "Database APIs for backup, import, and management")
public class DatabaseController {
private final DatabaseBackupHelper databaseBackupHelper;
private final DatabaseService databaseService;
public DatabaseController(DatabaseBackupHelper databaseBackupHelper) {
this.databaseBackupHelper = databaseBackupHelper;
public DatabaseController(DatabaseService databaseService) {
this.databaseService = databaseService;
}
@Operation(
@ -57,7 +57,7 @@ public class DatabaseController {
Path tempTemplatePath = Files.createTempFile("backup_", ".sql");
try (InputStream in = file.getInputStream()) {
Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING);
boolean importSuccess = databaseBackupHelper.importDatabaseFromUI(tempTemplatePath);
boolean importSuccess = databaseService.importDatabaseFromUI(tempTemplatePath);
if (importSuccess) {
redirectAttributes.addAttribute("infoMessage", "importIntoDatabaseSuccessed");
} else {
@ -84,14 +84,14 @@ public class DatabaseController {
}
// Check if the file exists in the backup list
boolean fileExists =
databaseBackupHelper.getBackupList().stream()
databaseService.getBackupList().stream()
.anyMatch(backup -> backup.getFileName().equals(fileName));
if (!fileExists) {
log.error("File {} not found in backup list", fileName);
return "redirect:/database?error=fileNotFound";
}
log.info("Received file: {}", fileName);
if (databaseBackupHelper.importDatabaseFromUI(fileName)) {
if (databaseService.importDatabaseFromUI(fileName)) {
log.info("File {} imported to database", fileName);
return "redirect:/database?infoMessage=importIntoDatabaseSuccessed";
}
@ -110,7 +110,7 @@ public class DatabaseController {
throw new IllegalArgumentException("File must not be null or empty");
}
try {
if (databaseBackupHelper.deleteBackupFile(fileName)) {
if (databaseService.deleteBackupFile(fileName)) {
log.info("Deleted file: {}", fileName);
} else {
log.error("Failed to delete file: {}", fileName);
@ -135,7 +135,7 @@ public class DatabaseController {
throw new IllegalArgumentException("File must not be null or empty");
}
try {
Path filePath = databaseBackupHelper.getBackupFilePath(fileName);
Path filePath = databaseService.getBackupFilePath(fileName);
InputStreamResource resource = new InputStreamResource(Files.newInputStream(filePath));
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName)
@ -157,14 +157,9 @@ public class DatabaseController {
+ " database management page.")
@GetMapping("/createDatabaseBackup")
public String createDatabaseBackup() {
try {
log.info("Starting database backup creation...");
databaseBackupHelper.exportDatabase();
log.info("Database backup successfully created.");
} catch (IOException e) {
log.error("Error creating database backup: {}", e.getMessage(), e);
return "redirect:/database?error=" + e.getMessage();
}
log.info("Starting database backup creation...");
databaseService.exportDatabase();
log.info("Database backup successfully created.");
return "redirect:/database?infoMessage=backupCreated";
}
}

View File

@ -2,6 +2,7 @@ package stirling.software.SPDF.controller.api;
import java.io.IOException;
import java.security.Principal;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -33,6 +34,7 @@ import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.model.api.user.UsernameAndPass;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
@Controller
@Tag(name = "User", description = "User APIs")
@ -52,7 +54,7 @@ public class UserController {
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/register")
public String register(@ModelAttribute UsernameAndPass requestModel, Model model)
throws IOException {
throws SQLException, UnsupportedProviderException {
if (userService.usernameExistsIgnoreCase(requestModel.getUsername())) {
model.addAttribute("error", "Username already exists");
return "register";
@ -74,7 +76,7 @@ public class UserController {
HttpServletRequest request,
HttpServletResponse response,
RedirectAttributes redirectAttributes)
throws IOException {
throws IOException, SQLException, UnsupportedProviderException {
if (!userService.isUsernameValid(newUsername)) {
return new RedirectView("/account?messageType=invalidUsername", true);
}
@ -117,7 +119,7 @@ public class UserController {
HttpServletRequest request,
HttpServletResponse response,
RedirectAttributes redirectAttributes)
throws IOException {
throws SQLException, UnsupportedProviderException {
if (principal == null) {
return new RedirectView("/change-creds?messageType=notAuthenticated", true);
}
@ -145,7 +147,7 @@ public class UserController {
HttpServletRequest request,
HttpServletResponse response,
RedirectAttributes redirectAttributes)
throws IOException {
throws SQLException, UnsupportedProviderException {
if (principal == null) {
return new RedirectView("/account?messageType=notAuthenticated", true);
}
@ -166,7 +168,7 @@ public class UserController {
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/updateUserSettings")
public String updateUserSettings(HttpServletRequest request, Principal principal)
throws IOException {
throws SQLException, UnsupportedProviderException {
Map<String, String[]> paramMap = request.getParameterMap();
Map<String, String> updates = new HashMap<>();
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
@ -188,7 +190,7 @@ public class UserController {
@RequestParam(name = "authType") String authType,
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
boolean forceChange)
throws IllegalArgumentException, IOException {
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!userService.isUsernameValid(username)) {
return new RedirectView("/addUsers?messageType=invalidUsername", true);
}
@ -232,7 +234,7 @@ public class UserController {
@RequestParam(name = "username") String username,
@RequestParam(name = "role") String role,
Authentication authentication)
throws IOException {
throws SQLException, UnsupportedProviderException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (!userOpt.isPresent()) {
return new RedirectView("/addUsers?messageType=userNotFound", true);
@ -270,7 +272,7 @@ public class UserController {
@PathVariable("username") String username,
@RequestParam("enabled") boolean enabled,
Authentication authentication)
throws IOException {
throws SQLException, UnsupportedProviderException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (!userOpt.isPresent()) {
return new RedirectView("/addUsers?messageType=userNotFound", true);

View File

@ -20,7 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletContext;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.SPdfApplication;
import stirling.software.SPDF.SPDFApplication;
import stirling.software.SPDF.model.ApiEndpoint;
import stirling.software.SPDF.model.Role;
@ -44,7 +44,7 @@ public class ApiDocService {
private String getApiDocsUrl() {
String contextPath = servletContext.getContextPath();
String port = SPdfApplication.getStaticPort();
String port = SPDFApplication.getStaticPort();
return "http://localhost:" + port + contextPath + "/v1/api-docs";
}

View File

@ -30,7 +30,7 @@ import io.github.pixee.security.ZipSecurity;
import jakarta.servlet.ServletContext;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.SPdfApplication;
import stirling.software.SPDF.SPDFApplication;
import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.PipelineOperation;
import stirling.software.SPDF.model.Role;
@ -80,7 +80,7 @@ public class PipelineProcessor {
private String getBaseUrl() {
String contextPath = servletContext.getContextPath();
String port = SPdfApplication.getStaticPort();
String port = SPDFApplication.getStaticPort();
return "http://localhost:" + port + contextPath + "/";
}

View File

@ -14,7 +14,11 @@ import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cms.*;
import org.bouncycastle.cms.CMSProcessable;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.cms.SignerInformationStore;
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
import org.bouncycastle.util.Store;
import org.springframework.beans.factory.annotation.Autowired;

View File

@ -11,17 +11,17 @@ import org.springframework.web.bind.annotation.GetMapping;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import stirling.software.SPDF.config.security.database.DatabaseBackupHelper;
import stirling.software.SPDF.config.security.database.DatabaseService;
import stirling.software.SPDF.utils.FileInfo;
@Controller
@Tag(name = "Database Management", description = "Database management and security APIs")
public class DatabaseWebController {
private final DatabaseBackupHelper databaseBackupHelper;
private final DatabaseService databaseService;
public DatabaseWebController(DatabaseBackupHelper databaseBackupHelper) {
this.databaseBackupHelper = databaseBackupHelper;
public DatabaseWebController(DatabaseService databaseService) {
this.databaseService = databaseService;
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@ -34,9 +34,9 @@ public class DatabaseWebController {
} else if (confirmed != null) {
model.addAttribute("infoMessage", confirmed);
}
List<FileInfo> backupList = databaseBackupHelper.getBackupList();
List<FileInfo> backupList = databaseService.getBackupList();
model.addAttribute("backupFiles", backupList);
model.addAttribute("databaseVersion", databaseBackupHelper.getH2Version());
model.addAttribute("databaseVersion", databaseService.getH2Version());
return "database";
}
}

View File

@ -0,0 +1,52 @@
package stirling.software.SPDF;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.junit.jupiter.api.BeforeEach;
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 org.springframework.core.env.Environment;
import stirling.software.SPDF.model.ApplicationProperties;
@ExtendWith(MockitoExtension.class)
public class SPDFApplicationTest {
@Mock
private Environment env;
@Mock
private ApplicationProperties applicationProperties;
@InjectMocks
private SPDFApplication SPDFApplication;
@BeforeEach
public void setUp() {
SPDFApplication.setServerPortStatic("8080");
}
@Test
public void testSetServerPortStatic() {
SPDFApplication.setServerPortStatic("9090");
assertEquals("9090", SPDFApplication.getStaticPort());
}
@Test
public void testGetStaticPort() {
assertEquals("8080", SPDFApplication.getStaticPort());
}
@Test
public void testGetNonStaticPort() {
assertEquals("8080", SPDFApplication.getNonStaticPort());
}
}

View File

@ -1,94 +0,0 @@
package stirling.software.SPDF;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.junit.jupiter.api.BeforeEach;
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 org.springframework.core.env.Environment;
import stirling.software.SPDF.UI.WebBrowser;
import stirling.software.SPDF.model.ApplicationProperties;
@ExtendWith(MockitoExtension.class)
public class SPdfApplicationTest {
@Mock
private Environment env;
@Mock
private ApplicationProperties applicationProperties;
@InjectMocks
private SPdfApplication sPdfApplication;
@BeforeEach
public void setUp() {
sPdfApplication.setServerPortStatic("8080");
}
@Test
public void testSetServerPortStatic() {
sPdfApplication.setServerPortStatic("9090");
assertEquals("9090", SPdfApplication.getStaticPort());
}
@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 (Files.exists(settingsPath)) {
Files.delete(settingsPath);
}
if (Files.exists(customSettingsPath)) {
Files.delete(customSettingsPath);
}
if (Files.exists(staticPath)) {
Files.delete(staticPath);
}
if (Files.exists(templatesPath)) {
Files.delete(templatesPath);
}
// Ensure the directories are created for testing
Files.createDirectories(configPath);
Files.createDirectories(staticPath);
Files.createDirectories(templatesPath);
Files.createFile(settingsPath);
Files.createFile(customSettingsPath);
// Run the main method
SPdfApplication.main(new String[]{});
// Verify that the directories were created
assertTrue(Files.exists(settingsPath));
assertTrue(Files.exists(customSettingsPath));
assertTrue(Files.exists(staticPath));
assertTrue(Files.exists(templatesPath));
}
@Test
public void testGetStaticPort() {
assertEquals("8080", SPdfApplication.getStaticPort());
}
@Test
public void testGetNonStaticPort() {
assertEquals("8080", sPdfApplication.getNonStaticPort());
}
}

View File

@ -1,6 +1,7 @@
package stirling.software.SPDF.config.security.database;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
@ -31,6 +32,7 @@ class DatabaseConfigTest {
}
@Test
@Disabled
void testDataSource_whenRunningEEIsFalse() throws UnsupportedProviderException {
databaseConfig = new DatabaseConfig(applicationProperties, false);
@ -93,7 +95,7 @@ class DatabaseConfigTest {
@ParameterizedTest(name = "Exception thrown when the DB type [{arguments}] is not supported")
@ValueSource(strings = {"oracle", "mysql", "mongoDb"})
void exceptionThrownWhenDBTypeIsUnsupported(String datasourceType) {
void exceptionThrown_whenDBTypeIsUnsupported(String datasourceType) {
var system = mock(ApplicationProperties.System.class);
var datasource = mock(ApplicationProperties.Datasource.class);

View File

@ -13,6 +13,7 @@ import static java.nio.file.Files.delete;
import static java.nio.file.Files.exists;
import static org.junit.jupiter.api.Assertions.assertTrue;
@Disabled
@SpringBootTest
public class SPDFApplicationIntegrationTest {