From e8430fb3514cdb45147cc572045d7cec60cfd138 Mon Sep 17 00:00:00 2001 From: Dario Ghunney Ware Date: Wed, 16 Apr 2025 16:56:18 +0100 Subject: [PATCH] wip - making db and sessions conditional --- .../SPDF/EE/KeygenLicenseVerifier.java | 68 +++++++++++-------- .../software/SPDF/SPDFApplication.java | 8 ++- .../config/security/InitialSecuritySetup.java | 42 ++++++++---- .../security/UserAuthenticationFilter.java | 2 + .../SPDF/config/security/UserService.java | 3 +- .../security/database/DatabaseConfig.java | 6 +- .../security/database/DatabaseService.java | 4 +- .../session/CustomHttpSessionListener.java | 4 +- .../session/SessionPersistentRegistry.java | 2 + .../session/SessionRegistryConfig.java | 2 + .../security/session/SessionRepository.java | 2 + .../security/session/SessionScheduled.java | 2 + src/main/resources/application.properties | 5 +- src/main/resources/settings.yml.template | 2 + 14 files changed, 103 insertions(+), 49 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/EE/KeygenLicenseVerifier.java b/src/main/java/stirling/software/SPDF/EE/KeygenLicenseVerifier.java index 20e3e803..8b68b860 100644 --- a/src/main/java/stirling/software/SPDF/EE/KeygenLicenseVerifier.java +++ b/src/main/java/stirling/software/SPDF/EE/KeygenLicenseVerifier.java @@ -53,24 +53,37 @@ public class KeygenLicenseVerifier { } public License verifyLicense(String licenseKeyOrCert) { + License license; + if (isCertificateLicense(licenseKeyOrCert)) { log.info("Detected certificate-based license. Processing..."); - return resultToEnum(verifyCertificateLicense(licenseKeyOrCert), License.ENTERPRISE); + boolean isValid = verifyCertificateLicense(licenseKeyOrCert); + if (isValid) { + license = isEnterpriseLicense ? License.ENTERPRISE : License.PRO; + } else { + license = License.NORMAL; + } } else if (isJWTLicense(licenseKeyOrCert)) { log.info("Detected JWT-style license key. Processing..."); - return resultToEnum(verifyJWTLicense(licenseKeyOrCert), License.ENTERPRISE); + boolean isValid = verifyJWTLicense(licenseKeyOrCert); + if (isValid) { + license = isEnterpriseLicense ? License.ENTERPRISE : License.PRO; + } else { + license = License.NORMAL; + } } else { log.info("Detected standard license key. Processing..."); - return resultToEnum(verifyStandardLicense(licenseKeyOrCert), License.PRO); + boolean isValid = verifyStandardLicense(licenseKeyOrCert); + if (isValid) { + license = isEnterpriseLicense ? License.ENTERPRISE : License.PRO; + } else { + license = License.NORMAL; + } } + return license; } - private License resultToEnum(boolean result, License option) { - if (result) { - return option; - } - return License.NORMAL; - } + private boolean isEnterpriseLicense = false; private boolean isCertificateLicense(String license) { return license != null && license.trim().startsWith(CERT_PREFIX); @@ -82,8 +95,6 @@ public class KeygenLicenseVerifier { private boolean verifyCertificateLicense(String licenseFile) { try { - log.info("Verifying certificate-based license"); - String encodedPayload = licenseFile; // Remove the header encodedPayload = encodedPayload.replace(CERT_PREFIX, ""); @@ -106,8 +117,6 @@ public class KeygenLicenseVerifier { encryptedData = (String) attrs.get("enc"); encodedSignature = (String) attrs.get("sig"); algorithm = (String) attrs.get("alg"); - - log.info("Certificate algorithm: {}", algorithm); } catch (JSONException e) { log.error("Failed to parse license file: {}", e.getMessage()); return false; @@ -151,7 +160,6 @@ public class KeygenLicenseVerifier { private boolean verifyEd25519Signature(String encryptedData, String encodedSignature) { try { log.info("Signature to verify: {}", encodedSignature); - log.info("Public key being used: {}", PUBLIC_KEY); byte[] signatureBytes = Base64.getDecoder().decode(encodedSignature); @@ -185,8 +193,6 @@ public class KeygenLicenseVerifier { private boolean processCertificateData(String certData) { try { - log.info("Processing certificate data: {}", certData); - JSONObject licenseData = new JSONObject(certData); JSONObject metaObj = licenseData.optJSONObject("meta"); if (metaObj != null) { @@ -234,18 +240,9 @@ public class KeygenLicenseVerifier { applicationProperties.getPremium().setMaxUsers(users); log.info("License allows for {} users", users); } + isEnterpriseLicense = metadataObj.optBoolean("isEnterprise", false); } - // Check maxUsers directly in attributes if present from policy definition - // if (attributesObj.has("maxUsers")) { - // int maxUsers = attributesObj.optInt("maxUsers", 0); - // if (maxUsers > 0) { - // applicationProperties.getPremium().setMaxUsers(maxUsers); - // log.info("License directly specifies {} max users", - // maxUsers); - // } - // } - // Check license status if available String status = attributesObj.optString("status", null); if (status != null @@ -388,9 +385,10 @@ public class KeygenLicenseVerifier { String policyId = policyObj.optString("id", "unknown"); log.info("License uses policy: {}", policyId); - // Extract max users from policy if available (customize based on your policy - // structure) + // Extract max users and isEnterprise from policy or metadata int users = policyObj.optInt("users", 0); + isEnterpriseLicense = policyObj.optBoolean("isEnterprise", false); + if (users > 0) { applicationProperties.getPremium().setMaxUsers(users); log.info("License allows for {} users", users); @@ -402,6 +400,9 @@ public class KeygenLicenseVerifier { users = metadata.optInt("users", 1); applicationProperties.getPremium().setMaxUsers(users); log.info("License allows for {} users (from metadata)", users); + + // Check for isEnterprise flag in metadata + isEnterpriseLicense = metadata.optBoolean("isEnterprise", false); } else { // Default value applicationProperties.getPremium().setMaxUsers(1); @@ -494,6 +495,7 @@ public class KeygenLicenseVerifier { log.info("Validation detail: " + detail); log.info("Validation code: " + code); + // Extract user count int users = jsonResponse .path("data") @@ -502,6 +504,16 @@ public class KeygenLicenseVerifier { .path("users") .asInt(0); applicationProperties.getPremium().setMaxUsers(users); + + // Extract isEnterprise flag + isEnterpriseLicense = + jsonResponse + .path("data") + .path("attributes") + .path("metadata") + .path("isEnterprise") + .asBoolean(false); + log.info(applicationProperties.toString()); } else { diff --git a/src/main/java/stirling/software/SPDF/SPDFApplication.java b/src/main/java/stirling/software/SPDF/SPDFApplication.java index 3cf89a65..34fb5257 100644 --- a/src/main/java/stirling/software/SPDF/SPDFApplication.java +++ b/src/main/java/stirling/software/SPDF/SPDFApplication.java @@ -14,6 +14,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; import org.springframework.core.env.Environment; import org.springframework.scheduling.annotation.EnableScheduling; @@ -32,7 +34,11 @@ import stirling.software.SPDF.utils.UrlUtils; @Slf4j @EnableScheduling -@SpringBootApplication +@SpringBootApplication( + exclude = { + DataSourceAutoConfiguration.class, + DataSourceTransactionManagerAutoConfiguration.class + }) public class SPDFApplication { private static String serverPortStatic; diff --git a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java index 9299d477..dfaa0782 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -3,10 +3,10 @@ package stirling.software.SPDF.config.security; import java.sql.SQLException; import java.util.UUID; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; -import jakarta.annotation.PostConstruct; - import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.interfaces.DatabaseInterface; @@ -14,15 +14,25 @@ import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.exception.UnsupportedProviderException; +/** + * This class is responsible for the initial security setup of the application. It checks if there + * are any existing users and initializes the admin user if none exist. It also migrates OAuth2 + * users to SSO and initializes an internal API user. + */ @Slf4j @Component +/* + todo: add @ConditionOnProperty to check if the application is running in a specific environment + add @Profile for enterprise/pro or higher +*/ +// @Profile({"pro", "enterprise"}) public class InitialSecuritySetup { private final UserService userService; private final ApplicationProperties applicationProperties; - private final DatabaseInterface databaseService; + @Lazy private final DatabaseInterface databaseService; public InitialSecuritySetup( UserService userService, @@ -33,19 +43,10 @@ public class InitialSecuritySetup { this.databaseService = databaseService; } - @PostConstruct + // @PostConstruct public void init() { try { - - if (!userService.hasUsers()) { - if (databaseService.hasBackup()) { - databaseService.importDatabase(); - } else { - initializeAdminUser(); - } - } - - userService.migrateOauth2ToSSO(); + initialiseDB(); initializeInternalApiUser(); } catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) { log.error("Failed to initialize security setup.", e); @@ -53,6 +54,19 @@ public class InitialSecuritySetup { } } + @ConditionalOnProperty(name = "premium.proFeatures.database", havingValue = "true") + private void initialiseDB() throws SQLException, UnsupportedProviderException { + if (!userService.hasUsers()) { + if (databaseService.hasBackup()) { + databaseService.importDatabase(); + } else { + initializeAdminUser(); + } + } + + userService.migrateOauth2ToSSO(); + } + private void initializeAdminUser() throws SQLException, UnsupportedProviderException { String initialUsername = applicationProperties.getSecurity().getInitialLogin().getUsername(); diff --git a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java index b0684d75..85668dd2 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Optional; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Lazy; import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; @@ -35,6 +36,7 @@ import stirling.software.SPDF.model.User; @Slf4j @Component +@ConditionalOnProperty(name = "premium.enabled", havingValue = "true") public class UserAuthenticationFilter extends OncePerRequestFilter { private final ApplicationProperties applicationProperties; diff --git a/src/main/java/stirling/software/SPDF/config/security/UserService.java b/src/main/java/stirling/software/SPDF/config/security/UserService.java index 46467671..457b71c9 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserService.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserService.java @@ -5,6 +5,7 @@ import java.sql.SQLException; import java.util.*; import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Lazy; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -44,7 +45,7 @@ public class UserService implements UserServiceInterface { private final SessionPersistentRegistry sessionRegistry; - private final DatabaseInterface databaseService; + @Lazy private final DatabaseInterface databaseService; private final ApplicationProperties applicationProperties; 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 index d221704e..5c0df766 100644 --- a/src/main/java/stirling/software/SPDF/config/security/database/DatabaseConfig.java +++ b/src/main/java/stirling/software/SPDF/config/security/database/DatabaseConfig.java @@ -3,9 +3,11 @@ package stirling.software.SPDF.config.security.database; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -16,7 +18,9 @@ import stirling.software.SPDF.model.exception.UnsupportedProviderException; @Slf4j @Getter +@Lazy @Configuration +@ConditionalOnProperty(name = "premium.proFeatures.database", havingValue = "true") public class DatabaseConfig { public final String DATASOURCE_DEFAULT_URL; @@ -35,7 +39,7 @@ public class DatabaseConfig { DATASOURCE_DEFAULT_URL = "jdbc:h2:file:" + InstallationPathConfig.getConfigPath() - + "stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"; + + "stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=PostgreSQL"; log.debug("Database URL: {}", DATASOURCE_DEFAULT_URL); this.applicationProperties = applicationProperties; this.runningProOrHigher = runningProOrHigher; diff --git a/src/main/java/stirling/software/SPDF/config/security/database/DatabaseService.java b/src/main/java/stirling/software/SPDF/config/security/database/DatabaseService.java index a8daede3..e0a6a9d2 100644 --- a/src/main/java/stirling/software/SPDF/config/security/database/DatabaseService.java +++ b/src/main/java/stirling/software/SPDF/config/security/database/DatabaseService.java @@ -21,6 +21,7 @@ import java.util.stream.Collectors; import javax.sql.DataSource; +import org.springframework.context.annotation.Lazy; import org.springframework.jdbc.datasource.init.CannotReadScriptException; import org.springframework.jdbc.datasource.init.ScriptException; import org.springframework.stereotype.Service; @@ -33,6 +34,7 @@ import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.exception.BackupNotFoundException; import stirling.software.SPDF.utils.FileInfo; +@Lazy @Slf4j @Service public class DatabaseService implements DatabaseInterface { @@ -42,7 +44,7 @@ public class DatabaseService implements DatabaseInterface { private final Path BACKUP_DIR; private final ApplicationProperties applicationProperties; - private final DataSource dataSource; + @Lazy private final DataSource dataSource; public DatabaseService(ApplicationProperties applicationProperties, DataSource dataSource) { this.BACKUP_DIR = diff --git a/src/main/java/stirling/software/SPDF/config/security/session/CustomHttpSessionListener.java b/src/main/java/stirling/software/SPDF/config/security/session/CustomHttpSessionListener.java index 3d97181a..76cf7997 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/CustomHttpSessionListener.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/CustomHttpSessionListener.java @@ -1,6 +1,7 @@ package stirling.software.SPDF.config.security.session; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import jakarta.servlet.http.HttpSessionEvent; @@ -8,8 +9,9 @@ import jakarta.servlet.http.HttpSessionListener; import lombok.extern.slf4j.Slf4j; -@Component @Slf4j +@Component +@ConditionalOnProperty(name = "premium.enabled", havingValue = "true") public class CustomHttpSessionListener implements HttpSessionListener { private SessionPersistentRegistry sessionPersistentRegistry; diff --git a/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java b/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java index 18b03716..fa7ee0df 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java @@ -4,6 +4,7 @@ import java.time.Duration; import java.util.*; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.security.core.session.SessionInformation; import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.core.userdetails.UserDetails; @@ -16,6 +17,7 @@ import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrin import stirling.software.SPDF.model.SessionEntity; @Component +@ConditionalOnProperty(name = "premium.enabled", havingValue = "true") public class SessionPersistentRegistry implements SessionRegistry { private final SessionRepository sessionRepository; diff --git a/src/main/java/stirling/software/SPDF/config/security/session/SessionRegistryConfig.java b/src/main/java/stirling/software/SPDF/config/security/session/SessionRegistryConfig.java index 8fa24e95..18a84279 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/SessionRegistryConfig.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/SessionRegistryConfig.java @@ -1,10 +1,12 @@ package stirling.software.SPDF.config.security.session; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.core.session.SessionRegistryImpl; @Configuration +@ConditionalOnProperty(name = "premium.enabled", havingValue = "true") public class SessionRegistryConfig { @Bean diff --git a/src/main/java/stirling/software/SPDF/config/security/session/SessionRepository.java b/src/main/java/stirling/software/SPDF/config/security/session/SessionRepository.java index b7f0133f..d4f2f4bc 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/SessionRepository.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/SessionRepository.java @@ -3,6 +3,7 @@ package stirling.software.SPDF.config.security.session; import java.util.Date; import java.util.List; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -14,6 +15,7 @@ import jakarta.transaction.Transactional; import stirling.software.SPDF.model.SessionEntity; @Repository +@ConditionalOnProperty(name = "premium.enabled", havingValue = "true") public interface SessionRepository extends JpaRepository { List findByPrincipalName(String principalName); diff --git a/src/main/java/stirling/software/SPDF/config/security/session/SessionScheduled.java b/src/main/java/stirling/software/SPDF/config/security/session/SessionScheduled.java index 9710316e..ddcb16d2 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/SessionScheduled.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/SessionScheduled.java @@ -5,11 +5,13 @@ import java.time.temporal.ChronoUnit; import java.util.Date; import java.util.List; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.security.core.session.SessionInformation; import org.springframework.stereotype.Component; @Component +@ConditionalOnProperty(name = "premium.enabled", havingValue = "true") public class SessionScheduled { private final SessionPersistentRegistry sessionPersistentRegistry; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 9f58a93c..217dc38f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -28,11 +28,12 @@ spring.thymeleaf.encoding=UTF-8 spring.web.resources.mime-mappings.webmanifest=application/manifest+json spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000} +management.endpoints.web.exposure.include=beans spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=PostgreSQL spring.datasource.driver-class-name=org.h2.Driver spring.datasource.username=sa spring.datasource.password= -spring.h2.console.enabled=false +spring.h2.console.enabled=true spring.jpa.hibernate.ddl-auto=update server.servlet.session.timeout:30m # Change the default URL path for OpenAPI JSON @@ -40,4 +41,4 @@ springdoc.api-docs.path=/v1/api-docs # Set the URL of the OpenAPI JSON for the Swagger UI springdoc.swagger-ui.url=/v1/api-docs posthog.api.key=phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq -posthog.host=https://eu.i.posthog.com \ No newline at end of file +posthog.host=https://eu.i.posthog.com diff --git a/src/main/resources/settings.yml.template b/src/main/resources/settings.yml.template index 827fec96..f03bf5ed 100644 --- a/src/main/resources/settings.yml.template +++ b/src/main/resources/settings.yml.template @@ -64,6 +64,7 @@ premium: key: 00000000-0000-0000-0000-000000000000 enabled: false # Enable license key checks for pro/enterprise features proFeatures: + database: false # Enable database features SSOAutoLogin: false CustomMetadata: autoUpdateMetadata: false @@ -95,6 +96,7 @@ system: enableUrlToPDF: false # Set to 'true' to enable URL to PDF, INTERNAL ONLY, known security issues, should not be used externally disableSanitize: false # set to true to disable Sanitize HTML; (can lead to injections in HTML) datasource: + enabled: true # set to 'true' to enable the database connection enableCustomDatabase: false # Enterprise users ONLY, set this property to 'true' if you would like to use your own custom database configuration customDatabaseUrl: '' # eg jdbc:postgresql://localhost:5432/postgres, set the url for your own custom database connection. If provided, the type, hostName, port and name are not necessary and will not be used username: postgres # set the database username