feat(docker): update base images to Java 25, Spring 4, Jackson 3, Gradle 9 and optimize JVM options (Project Lilliput) (#5725)

Co-authored-by: Anthony Stirling <[email protected]>
This commit is contained in:
Balázs Szücs
2026-02-24 20:06:32 +00:00
committed by GitHub
co-authored by Anthony Stirling
parent 24128dd318
commit 1f9b90ad57
112 changed files with 3181 additions and 2102 deletions
@@ -2,21 +2,22 @@ package stirling.software.proprietary.config;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.slf4j.MDC;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskDecorator;
import org.springframework.core.task.support.TaskExecutorAdapter;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@EnableAsync
public class AsyncConfig {
/**
* MDC context-propagating task decorator Copies MDC context from the caller thread to the async
* executor thread
* MDC context-propagating task decorator. Copies MDC context from the caller thread to the
* virtual thread executing the task.
*/
static class MDCContextTaskDecorator implements TaskDecorator {
@Override
@@ -42,16 +43,9 @@ public class AsyncConfig {
@Bean(name = "auditExecutor")
public Executor auditExecutor() {
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
exec.setCorePoolSize(2);
exec.setMaxPoolSize(8);
exec.setQueueCapacity(1_000);
exec.setThreadNamePrefix("audit-");
// Set the task decorator to propagate MDC context
exec.setTaskDecorator(new MDCContextTaskDecorator());
exec.initialize();
return exec;
TaskExecutorAdapter adapter =
new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
adapter.setTaskDecorator(new MDCContextTaskDecorator());
return adapter;
}
}
@@ -1,15 +1,12 @@
package stirling.software.proprietary.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/** Configuration to enable scheduling for the audit system. */
/** Configuration for audit system transaction management. */
@Configuration
@EnableTransactionManagement
@EnableScheduling
public class AuditJpaConfig {
// This configuration enables scheduling for audit cleanup tasks
// JPA repositories are now managed by DatabaseConfig to avoid conflicts
// No additional beans or methods needed
// Scheduling is enabled on SPDFApplication — no duplicate @EnableScheduling needed.
// JPA repositories are now managed by DatabaseConfig to avoid conflicts.
}
@@ -12,8 +12,6 @@ import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -21,6 +19,8 @@ import stirling.software.proprietary.model.security.PersistentAuditEvent;
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
import stirling.software.proprietary.util.SecretMasker;
import tools.jackson.databind.ObjectMapper;
@Component
@Primary
@RequiredArgsConstructor
@@ -68,7 +68,10 @@ public class CustomAuditEventRepository implements AuditEventRepository {
.build();
repo.save(ent);
} catch (Exception e) {
e.printStackTrace(); // fail-open
log.error(
"Failed to persist audit event (fail-open); principal={}",
ev.getPrincipal(),
e);
}
}
}
@@ -28,9 +28,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -47,6 +44,9 @@ import stirling.software.proprietary.model.security.PersistentAuditEvent;
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.ObjectMapper;
/** REST endpoints for the audit dashboard. */
@Slf4j
@RestController
@@ -266,7 +266,7 @@ public class AuditDashboardController {
headers.setContentDispositionFormData("attachment", "audit_export.json");
return ResponseEntity.ok().headers(headers).body(jsonBytes);
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
log.error("Error serializing audit events to JSON", e);
return ResponseEntity.internalServerError().build();
}
@@ -20,9 +20,6 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -32,6 +29,9 @@ import stirling.software.proprietary.model.security.PersistentAuditEvent;
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.ObjectMapper;
/** REST API controller for audit data used by React frontend. */
@Slf4j
@ProprietaryUiDataApi
@@ -332,7 +332,7 @@ public class AuditRestController {
@SuppressWarnings("unchecked")
Map<String, Object> parsed = objectMapper.readValue(event.getData(), Map.class);
details = parsed;
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
log.warn("Failed to parse audit event data as JSON: {}", event.getData());
details.put("rawData", event.getData());
}
@@ -380,7 +380,7 @@ public class AuditRestController {
headers.setContentDispositionFormData("attachment", "audit_export.json");
return ResponseEntity.ok().headers(headers).body(jsonBytes);
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
log.error("Error serializing audit events to JSON", e);
return ResponseEntity.internalServerError().build();
}
@@ -15,9 +15,6 @@ import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import lombok.Data;
@@ -54,6 +51,9 @@ import stirling.software.proprietary.security.service.TeamService;
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
import stirling.software.proprietary.service.UserLicenseSettingsService;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.ObjectMapper;
@Slf4j
@ProprietaryUiDataApi
public class ProprietaryUIDataController {
@@ -414,7 +414,7 @@ public class ProprietaryUIDataController {
String settingsJson;
try {
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
log.error("Error converting settings map", e);
return ResponseEntity.status(500).build();
}
@@ -8,9 +8,6 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -20,6 +17,9 @@ import stirling.software.proprietary.model.security.PersistentAuditEvent;
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.ObjectMapper;
/** REST API controller for usage analytics data used by React frontend. */
@Slf4j
@ProprietaryUiDataApi
@@ -135,7 +135,7 @@ public class UsageRestController {
return normalizeEndpoint(requestUri.toString());
}
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
log.debug("Failed to parse audit data JSON: {}", dataJson, e);
}
@@ -6,9 +6,9 @@ import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.jdbc.DatabaseDriver;
import org.springframework.boot.persistence.autoconfigure.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@@ -75,10 +75,18 @@ public class DatabaseConfig {
}
private DataSource useDefaultDataSource(DataSourceBuilder<?> dataSourceBuilder) {
// Support AOT training: override URL via system property to avoid H2 file lock
// conflicts when the AOT RECORD phase starts a second Spring context
String overrideUrl = System.getProperty("stirling.datasource.url");
String url =
(overrideUrl != null && !overrideUrl.isBlank())
? overrideUrl
: DATASOURCE_DEFAULT_URL;
log.info("Using default H2 database");
dataSourceBuilder
.url(DATASOURCE_DEFAULT_URL)
.url(url)
.driverClassName(DatabaseDriver.H2.getDriverClassName())
.username(DEFAULT_USERNAME);
@@ -14,15 +14,17 @@ import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.CorsConfigurer;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml5AuthenticationRequestResolver;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
@@ -82,7 +84,7 @@ public class SecurityConfiguration {
private final PersistentLoginRepository persistentLoginRepository;
private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper;
private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations;
private final OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver;
private final OpenSaml5AuthenticationRequestResolver saml2AuthenticationRequestResolver;
private final stirling.software.proprietary.service.UserLicenseSettingsService
licenseSettingsService;
private final ClientRegistrationRepository clientRegistrationRepository;
@@ -105,7 +107,7 @@ public class SecurityConfiguration {
@Autowired(required = false)
RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations,
@Autowired(required = false)
OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver,
OpenSaml5AuthenticationRequestResolver saml2AuthenticationRequestResolver,
@Autowired(required = false) ClientRegistrationRepository clientRegistrationRepository,
stirling.software.proprietary.service.UserLicenseSettingsService
licenseSettingsService) {
@@ -151,7 +153,8 @@ public class SecurityConfiguration {
// Default to allowing all origins when nothing is configured
cfg.setAllowedOriginPatterns(List.of("*"));
log.info(
"No CORS allowed origins configured in settings.yml (system.corsAllowedOrigins); allowing all origins.");
"No CORS allowed origins configured in settings.yml"
+ " (system.corsAllowedOrigins); allowing all origins.");
}
// Explicitly configure supported HTTP methods (include OPTIONS for preflight)
@@ -225,7 +228,7 @@ public class SecurityConfiguration {
http.cors(cors -> cors.configurationSource(corsSource));
} else {
// Explicitly disable CORS when no origins are configured
http.cors(cors -> cors.disable());
http.cors(CorsConfigurer::disable);
}
http.csrf(CsrfConfigurer::disable);
@@ -233,24 +236,24 @@ public class SecurityConfiguration {
// Configure X-Frame-Options based on settings.yml configuration
// When login is disabled, automatically disable X-Frame-Options to allow embedding
if (!loginEnabledValue) {
http.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.disable()));
http.headers(headers -> headers.frameOptions(FrameOptionsConfig::disable));
} else {
String xFrameOption = securityProperties.getXFrameOptions();
if (xFrameOption != null) {
http.headers(
headers -> {
if ("DISABLED".equalsIgnoreCase(xFrameOption)) {
headers.frameOptions(frameOptions -> frameOptions.disable());
headers.frameOptions(FrameOptionsConfig::disable);
} else if ("SAMEORIGIN".equalsIgnoreCase(xFrameOption)) {
headers.frameOptions(frameOptions -> frameOptions.sameOrigin());
headers.frameOptions(FrameOptionsConfig::sameOrigin);
} else {
// Default to DENY
headers.frameOptions(frameOptions -> frameOptions.deny());
headers.frameOptions(FrameOptionsConfig::deny);
}
});
} else {
// If not configured, use default DENY
http.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.deny()));
http.headers(headers -> headers.frameOptions(FrameOptionsConfig::deny));
}
}
@@ -381,8 +384,8 @@ public class SecurityConfiguration {
}
// Handle SAML
if (securityProperties.isSaml2Active() && runningProOrHigher) {
OpenSaml4AuthenticationProvider authenticationProvider =
new OpenSaml4AuthenticationProvider();
OpenSaml5AuthenticationProvider authenticationProvider =
new OpenSaml5AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter(
new CustomSaml2ResponseAuthenticationConverter(userService));
http.authenticationProvider(authenticationProvider)
@@ -12,11 +12,6 @@ import org.bouncycastle.crypto.signers.Ed25519Signer;
import org.bouncycastle.util.encoders.Hex;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.posthog.java.shaded.org.json.JSONException;
import com.posthog.java.shaded.org.json.JSONObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -24,6 +19,9 @@ import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.RegexPatternUtils;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
@Service
@Slf4j
@RequiredArgsConstructor
@@ -47,7 +45,7 @@ public class KeygenLicenseVerifier {
private static final String JWT_PREFIX = "key/";
private static final ObjectMapper objectMapper = new ObjectMapper();
private final ObjectMapper objectMapper;
private final ApplicationProperties applicationProperties;
// Shared HTTP client for connection pooling
@@ -135,11 +133,11 @@ public class KeygenLicenseVerifier {
String algorithm = "";
try {
JSONObject attrs = new JSONObject(payload);
encryptedData = (String) attrs.get("enc");
encodedSignature = (String) attrs.get("sig");
algorithm = (String) attrs.get("alg");
} catch (JSONException e) {
JsonNode attrs = objectMapper.readTree(payload);
encryptedData = attrs.path("enc").asText("");
encodedSignature = attrs.path("sig").asText("");
algorithm = attrs.path("alg").asText("");
} catch (Exception e) {
log.error("Failed to parse license file: {}", e.getMessage());
return false;
}
@@ -215,11 +213,17 @@ public class KeygenLicenseVerifier {
private boolean processCertificateData(String certData, LicenseContext context) {
try {
JSONObject licenseData = new JSONObject(certData);
JSONObject metaObj = licenseData.optJSONObject("meta");
if (metaObj != null) {
String issuedStr = metaObj.optString("issued", null);
String expiryStr = metaObj.optString("expiry", null);
JsonNode licenseData = objectMapper.readTree(certData);
JsonNode metaObj = licenseData.path("meta");
if (!metaObj.isMissingNode() && metaObj.isObject()) {
String issuedStr =
metaObj.path("issued").isNull()
? null
: metaObj.path("issued").asText(null);
String expiryStr =
metaObj.path("expiry").isNull()
? null
: metaObj.path("expiry").asText(null);
if (issuedStr != null && expiryStr != null) {
java.time.Instant issued = java.time.Instant.parse(issuedStr);
@@ -244,31 +248,32 @@ public class KeygenLicenseVerifier {
}
// Get the main license data
JSONObject dataObj = licenseData.optJSONObject("data");
if (dataObj == null) {
JsonNode dataObj = licenseData.path("data");
if (dataObj.isMissingNode() || !dataObj.isObject()) {
log.error("No data object found in certificate");
return false;
}
// Extract license or machine information
JSONObject attributesObj = dataObj.optJSONObject("attributes");
if (attributesObj != null) {
JsonNode attributesObj = dataObj.path("attributes");
if (!attributesObj.isMissingNode() && attributesObj.isObject()) {
log.info("Found attributes in certificate data");
// Check for floating license
context.isFloatingLicense = attributesObj.optBoolean("floating", false);
context.maxMachines = attributesObj.optInt("maxMachines", 1);
context.isFloatingLicense = attributesObj.path("floating").asBoolean(false);
context.maxMachines = attributesObj.path("maxMachines").asInt(1);
// Extract metadata
JSONObject metadataObj = attributesObj.optJSONObject("metadata");
if (metadataObj != null) {
JsonNode metadataObj = attributesObj.path("metadata");
if (!metadataObj.isMissingNode() && metadataObj.isObject()) {
// Check if this is an old license (no planType) with isEnterprise flag
context.isEnterpriseLicense = metadataObj.optBoolean("isEnterprise", false);
context.isEnterpriseLicense = metadataObj.path("isEnterprise").asBoolean(false);
// Extract user count - default based on license type
// Old licenses: Only had isEnterprise flag
// New licenses: Have planType field with "server" or "enterprise"
int users = metadataObj.optInt("users", context.isEnterpriseLicense ? 1 : 0);
int users =
metadataObj.path("users").asInt(context.isEnterpriseLicense ? 1 : 0);
// SERVER license (isEnterprise=false, users=0) = unlimited
// ENTERPRISE license (isEnterprise=true, users>0) = limited seats
@@ -282,7 +287,7 @@ public class KeygenLicenseVerifier {
}
// Check license status if available
String status = attributesObj.optString("status", null);
String status = attributesObj.path("status").asText(null);
if (status != null
&& !"ACTIVE".equals(status)
&& !"EXPIRING".equals(status)) { // Accept "EXPIRING" status as valid
@@ -372,11 +377,11 @@ public class KeygenLicenseVerifier {
try {
log.info("Processing license payload: {}", payload);
JSONObject licenseData = new JSONObject(payload);
JsonNode licenseData = objectMapper.readTree(payload);
JSONObject licenseObj = licenseData.optJSONObject("license");
if (licenseObj == null) {
String id = licenseData.optString("id", null);
JsonNode licenseObj = licenseData.path("license");
if (licenseObj.isMissingNode() || !licenseObj.isObject()) {
String id = licenseData.path("id").asText(null);
if (id != null) {
log.info("Found license ID: {}", id);
licenseObj = licenseData; // Use the root object as the license object
@@ -386,18 +391,18 @@ public class KeygenLicenseVerifier {
}
}
String licenseId = licenseObj.optString("id", "unknown");
String licenseId = licenseObj.path("id").asText("unknown");
log.info("Processing license with ID: {}", licenseId);
// Check for floating license in license object
context.isFloatingLicense = licenseObj.optBoolean("floating", false);
context.maxMachines = licenseObj.optInt("maxMachines", 1);
context.isFloatingLicense = licenseObj.path("floating").asBoolean(false);
context.maxMachines = licenseObj.path("maxMachines").asInt(1);
if (context.isFloatingLicense) {
log.info("Detected floating license with max machines: {}", context.maxMachines);
}
// Check expiry date
String expiryStr = licenseObj.optString("expiry", null);
String expiryStr = licenseObj.path("expiry").asText(null);
if (expiryStr != null && !"null".equals(expiryStr)) {
java.time.Instant expiry = java.time.Instant.parse(expiryStr);
java.time.Instant now = java.time.Instant.now();
@@ -413,9 +418,9 @@ public class KeygenLicenseVerifier {
}
// Extract account, product, policy info
JSONObject accountObj = licenseData.optJSONObject("account");
if (accountObj != null) {
String accountId = accountObj.optString("id", "unknown");
JsonNode accountObj = licenseData.path("account");
if (!accountObj.isMissingNode() && accountObj.isObject()) {
String accountId = accountObj.path("id").asText("unknown");
log.info("License belongs to account: {}", accountId);
// Verify this matches your expected account ID
@@ -426,14 +431,14 @@ public class KeygenLicenseVerifier {
}
// Extract policy information if available
JSONObject policyObj = licenseData.optJSONObject("policy");
if (policyObj != null) {
String policyId = policyObj.optString("id", "unknown");
JsonNode policyObj = licenseData.path("policy");
if (!policyObj.isMissingNode() && policyObj.isObject()) {
String policyId = policyObj.path("id").asText("unknown");
log.info("License uses policy: {}", policyId);
// Check for floating license in policy
boolean policyFloating = policyObj.optBoolean("floating", false);
int policyMaxMachines = policyObj.optInt("maxMachines", 1);
boolean policyFloating = policyObj.path("floating").asBoolean(false);
int policyMaxMachines = policyObj.path("maxMachines").asInt(1);
// Policy settings take precedence
if (policyFloating) {
@@ -445,16 +450,21 @@ public class KeygenLicenseVerifier {
}
// Extract max users and isEnterprise from policy or metadata
context.isEnterpriseLicense = policyObj.optBoolean("isEnterprise", false);
int users = policyObj.optInt("users", -1);
context.isEnterpriseLicense = policyObj.path("isEnterprise").asBoolean(false);
int users = policyObj.path("users").asInt(-1);
if (users == -1) {
// Try to get users from metadata if not at policy level
Object metadataObj = policyObj.opt("metadata");
if (metadataObj instanceof JSONObject metadata) {
JsonNode metadataObj = policyObj.path("metadata");
if (!metadataObj.isMissingNode() && metadataObj.isObject()) {
context.isEnterpriseLicense =
metadata.optBoolean("isEnterprise", context.isEnterpriseLicense);
users = metadata.optInt("users", context.isEnterpriseLicense ? 1 : 0);
metadataObj
.path("isEnterprise")
.asBoolean(context.isEnterpriseLicense);
users =
metadataObj
.path("users")
.asInt(context.isEnterpriseLicense ? 1 : 0);
} else {
// Default based on license type
users = context.isEnterpriseLicense ? 1 : 0;
@@ -493,9 +503,9 @@ public class KeygenLicenseVerifier {
validateLicense(licenseKey, machineFingerprint, context);
if (validationResponse != null) {
boolean isValid = validationResponse.path("meta").path("valid").asBoolean();
String licenseId = validationResponse.path("data").path("id").asText();
String licenseId = validationResponse.path("data").path("id").asText("");
if (!isValid) {
String code = validationResponse.path("meta").path("code").asText();
String code = validationResponse.path("meta").path("code").asText("");
log.info(code);
if ("NO_MACHINE".equals(code)
|| "NO_MACHINES".equals(code)
@@ -579,8 +589,8 @@ public class KeygenLicenseVerifier {
JsonNode metaNode = jsonResponse.path("meta");
boolean isValid = metaNode.path("valid").asBoolean();
String detail = metaNode.path("detail").asText();
String code = metaNode.path("code").asText();
String detail = metaNode.path("detail").asText("");
String code = metaNode.path("code").asText("");
log.info("License validity: {}", isValid);
log.info("Validation detail: {}", detail);
@@ -604,7 +614,7 @@ public class KeygenLicenseVerifier {
if (includedNode.isArray()) {
for (JsonNode node : includedNode) {
if ("policies".equals(node.path("type").asText())) {
if ("policies".equals(node.path("type").asText(""))) {
policyNode = node;
break;
}
@@ -690,9 +700,9 @@ public class KeygenLicenseVerifier {
for (JsonNode machine : machines) {
if (machineFingerprint.equals(
machine.path("attributes").path("fingerprint").asText())) {
machine.path("attributes").path("fingerprint").asText(""))) {
isCurrentMachineActivated = true;
currentMachineId = machine.path("id").asText();
currentMachineId = machine.path("id").asText("");
log.info(
"Current machine is already activated with ID: {}",
currentMachineId);
@@ -726,7 +736,7 @@ public class KeygenLicenseVerifier {
java.time.Instant.parse(createdStr);
if (oldestTime == null || createdTime.isBefore(oldestTime)) {
oldestTime = createdTime;
oldestMachineId = machine.path("id").asText();
oldestMachineId = machine.path("id").asText("");
}
} catch (Exception e) {
log.warn(
@@ -740,7 +750,7 @@ public class KeygenLicenseVerifier {
if (oldestMachineId == null) {
log.warn(
"Could not determine oldest machine by timestamp, using first machine in list");
oldestMachineId = machines.path(0).path("id").asText();
oldestMachineId = machines.path(0).path("id").asText("");
}
log.info("Deregistering machine with ID: {}", oldestMachineId);
@@ -770,35 +780,28 @@ public class KeygenLicenseVerifier {
hostname = "Unknown";
}
JSONObject body =
new JSONObject()
.put(
"data",
new JSONObject()
.put("type", "machines")
.put(
"attributes",
new JSONObject()
.put("fingerprint", machineFingerprint)
.put(
"platform",
System.getProperty("os.name"))
.put("name", hostname))
.put(
"relationships",
new JSONObject()
.put(
"license",
new JSONObject()
.put(
"data",
new JSONObject()
.put(
"type",
"licenses")
.put(
"id",
licenseId)))));
tools.jackson.databind.node.ObjectNode attributes = objectMapper.createObjectNode();
attributes.put("fingerprint", machineFingerprint);
attributes.put("platform", System.getProperty("os.name"));
attributes.put("name", hostname);
tools.jackson.databind.node.ObjectNode licenseRef = objectMapper.createObjectNode();
licenseRef.put("type", "licenses");
licenseRef.put("id", licenseId);
tools.jackson.databind.node.ObjectNode licenseRelation = objectMapper.createObjectNode();
licenseRelation.set("data", licenseRef);
tools.jackson.databind.node.ObjectNode relationships = objectMapper.createObjectNode();
relationships.set("license", licenseRelation);
tools.jackson.databind.node.ObjectNode data = objectMapper.createObjectNode();
data.put("type", "machines");
data.set("attributes", attributes);
data.set("relationships", relationships);
tools.jackson.databind.node.ObjectNode body = objectMapper.createObjectNode();
body.set("data", data);
HttpRequest request =
HttpRequest.newBuilder()
@@ -27,9 +27,6 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.util.HtmlUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
@@ -49,6 +46,9 @@ import stirling.software.proprietary.security.model.api.admin.SettingValueRespon
import stirling.software.proprietary.security.model.api.admin.UpdateSettingValueRequest;
import stirling.software.proprietary.security.model.api.admin.UpdateSettingsRequest;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
@AdminApi
@RequiredArgsConstructor
@PreAuthorize("hasRole('ROLE_ADMIN')")
@@ -543,7 +543,8 @@ public class AdminSettingsController {
pendingChanges.clear();
// Give the HTTP response time to complete, then exit
new Thread(
Thread.ofVirtual()
.start(
() -> {
try {
Thread.sleep(1000);
@@ -554,8 +555,7 @@ public class AdminSettingsController {
log.error("Restart interrupted: {}", e.getMessage(), e);
Thread.currentThread().interrupt();
}
})
.start();
});
return ResponseEntity.ok(
Map.of(
@@ -20,9 +20,6 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import lombok.Data;
@@ -31,6 +28,9 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.common.configuration.RuntimePathConfig;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
@Slf4j
@RestController
@RequestMapping("/api/v1/ui-data")
@@ -39,6 +39,7 @@ public class UIDataTessdataController {
private static final Pattern INVALID_LANG_CHARS_PATTERN = Pattern.compile("[^A-Za-z0-9_+\\-]");
private final RuntimePathConfig runtimePathConfig;
private final ObjectMapper objectMapper;
private static volatile List<String> cachedRemoteTessdata = null;
private static volatile long cachedRemoteTessdataExpiry = 0L;
private static final long REMOTE_TESSDATA_TTL_MS = 10 * 60 * 1000; // 10 minutes
@@ -223,9 +224,9 @@ public class UIDataTessdataController {
}
try (InputStream is = connection.getInputStream()) {
ObjectMapper mapper = new ObjectMapper();
List<Map<String, Object>> items =
mapper.readValue(is, new TypeReference<List<Map<String, Object>>>() {});
objectMapper.readValue(
is, new TypeReference<List<Map<String, Object>>>() {});
List<String> languages =
items.stream()
.map(item -> (String) item.get("name"))
@@ -11,7 +11,7 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
private Object credentials;
public ApiKeyAuthenticationToken(String apiKey) {
super(null);
super((Collection<? extends GrantedAuthority>) null);
this.principal = null;
this.credentials = apiKey;
setAuthenticated(false);
@@ -14,7 +14,7 @@ import org.opensaml.saml.saml2.core.AuthnStatement;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider.ResponseToken;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import lombok.RequiredArgsConstructor;
@@ -15,7 +15,7 @@ import org.springframework.security.saml2.provider.service.registration.InMemory
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml5AuthenticationRequestResolver;
import jakarta.servlet.http.HttpServletRequest;
@@ -151,10 +151,10 @@ public class Saml2Configuration {
@Bean
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
public OpenSaml5AuthenticationRequestResolver authenticationRequestResolver(
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
OpenSaml4AuthenticationRequestResolver resolver =
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
OpenSaml5AuthenticationRequestResolver resolver =
new OpenSaml5AuthenticationRequestResolver(relyingPartyRegistrationRepository);
resolver.setRelayStateResolver(
request -> {
@@ -20,9 +20,6 @@ import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
@@ -40,21 +37,25 @@ import stirling.software.proprietary.security.model.JwtVerificationKey;
import stirling.software.proprietary.security.model.exception.AuthenticationFailureException;
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
@Slf4j
@Service
public class JwtService implements JwtServiceInterface {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final ObjectMapper objectMapper;
private final KeyPersistenceServiceInterface keyPersistenceService;
private final boolean v2Enabled;
private final ApplicationProperties.Security securityProperties;
@Autowired
public JwtService(
ObjectMapper objectMapper,
@Qualifier("v2Enabled") boolean v2Enabled,
KeyPersistenceServiceInterface keyPersistenceService,
ApplicationProperties applicationProperties) {
this.objectMapper = objectMapper;
this.v2Enabled = v2Enabled;
this.keyPersistenceService = keyPersistenceService;
this.securityProperties = applicationProperties.getSecurity();
@@ -374,14 +375,14 @@ public class JwtService implements JwtServiceInterface {
byte[] headerBytes = Base64.getUrlDecoder().decode(tokenParts[0]);
Map<String, Object> header =
OBJECT_MAPPER.readValue(
objectMapper.readValue(
headerBytes, new TypeReference<Map<String, Object>>() {});
Object keyId = header.get("kid");
return keyId instanceof String ? (String) keyId : null;
} catch (IllegalArgumentException e) {
log.debug("Failed to decode Base64 JWT header: {}", e.getMessage());
return null;
} catch (java.io.IOException e) {
} catch (tools.jackson.core.JacksonException e) {
log.debug("Failed to parse JWT header as JSON: {}", e.getMessage());
return null;
}
@@ -15,8 +15,6 @@ import java.util.stream.Stream;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.configuration.InstallationPathConfig;
@@ -24,6 +22,8 @@ import stirling.software.common.service.PersonalSignatureServiceInterface;
import stirling.software.proprietary.model.api.signature.SavedSignatureRequest;
import stirling.software.proprietary.model.api.signature.SavedSignatureResponse;
import tools.jackson.databind.ObjectMapper;
/**
* Service for managing user signatures with authentication and storage limits. This proprietary
* version enforces per-user quotas and requires authentication. Provides access to personal
@@ -36,15 +36,16 @@ public class SignatureService implements PersonalSignatureServiceInterface {
private static final Pattern FILENAME_VALIDATION_PATTERN = Pattern.compile("^[a-zA-Z0-9_.-]+$");
private final String SIGNATURE_BASE_PATH;
private final String ALL_USERS_FOLDER = "ALL_USERS";
private final ObjectMapper objectMapper = new ObjectMapper();
private final ObjectMapper objectMapper;
// Storage limits per user
private static final int MAX_SIGNATURES_PER_USER = 20;
private static final long MAX_SIGNATURE_SIZE_BYTES = 2_000_000; // 2MB per signature
private static final long MAX_TOTAL_USER_STORAGE_BYTES = 20_000_000; // 20MB total per user
public SignatureService() {
public SignatureService(ObjectMapper objectMapper) {
SIGNATURE_BASE_PATH = InstallationPathConfig.getSignaturesPath();
this.objectMapper = objectMapper;
}
/**
@@ -16,8 +16,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import com.fasterxml.jackson.databind.ObjectMapper;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.enumeration.Role;
import stirling.software.proprietary.config.AuditConfigurationProperties;
@@ -35,6 +33,9 @@ import stirling.software.proprietary.security.service.MfaService;
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
import stirling.software.proprietary.service.UserLicenseSettingsService;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
@ExtendWith(MockitoExtension.class)
class ProprietaryUIDataControllerTest {
@@ -63,7 +64,7 @@ class ProprietaryUIDataControllerTest {
applicationProperties.getSecurity().getSaml2().setEnabled(false);
auditConfig = new AuditConfigurationProperties(applicationProperties);
objectMapper = new ObjectMapper();
objectMapper = JsonMapper.builder().build();
controller =
new ProprietaryUIDataController(
@@ -26,8 +26,6 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import com.fasterxml.jackson.databind.ObjectMapper;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.enumeration.Role;
import stirling.software.proprietary.security.model.AuthenticationType;
@@ -42,10 +40,13 @@ import stirling.software.proprietary.security.service.RefreshRateLimitService;
import stirling.software.proprietary.security.service.TotpService;
import stirling.software.proprietary.security.service.UserService;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
@ExtendWith(MockitoExtension.class)
class AuthControllerLoginTest {
private final ObjectMapper objectMapper = new ObjectMapper();
private final ObjectMapper objectMapper = JsonMapper.builder().build();
private MockMvc mockMvc;
private ApplicationProperties.Security securityProperties;
@@ -26,8 +26,6 @@ import org.springframework.security.core.Authentication;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import com.fasterxml.jackson.databind.ObjectMapper;
import stirling.software.proprietary.security.model.AuthenticationType;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.service.CustomUserDetailsService;
@@ -37,12 +35,15 @@ import stirling.software.proprietary.security.service.MfaService;
import stirling.software.proprietary.security.service.TotpService;
import stirling.software.proprietary.security.service.UserService;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
@ExtendWith(MockitoExtension.class)
class AuthControllerMfaTest {
private static final String USERNAME = "[email protected]";
private final ObjectMapper objectMapper = new ObjectMapper();
private final ObjectMapper objectMapper = JsonMapper.builder().build();
private MockMvc mockMvc;
private Authentication authentication;
@@ -19,6 +19,8 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import stirling.software.common.configuration.RuntimePathConfig;
import tools.jackson.databind.json.JsonMapper;
class UIDataTessdataControllerTest {
@Test
@@ -27,7 +29,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn("ignored/path");
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected List<String> getRemoteTessdataLanguages() {
return List.of("eng");
@@ -49,7 +51,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected List<String> getRemoteTessdataLanguages() {
return List.of("eng");
@@ -77,7 +79,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected List<String> getRemoteTessdataLanguages() {
return List.of("eng");
@@ -100,7 +102,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected List<String> getRemoteTessdataLanguages() {
return List.of("eng", "fra");
@@ -139,7 +141,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected List<String> getRemoteTessdataLanguages() {
return List.of("eng");
@@ -164,7 +166,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected boolean isWritableDirectory(Path dir) {
return false;
@@ -186,7 +188,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected List<String> getRemoteTessdataLanguages() {
return List.of("eng");
@@ -217,7 +219,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected List<String> getRemoteTessdataLanguages() {
return List.of("eng");
@@ -258,7 +260,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected List<String> getRemoteTessdataLanguages() {
return List.of("eng", "fra");
@@ -282,7 +284,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected List<String> getRemoteTessdataLanguages() {
return List.of("eng");
@@ -307,7 +309,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected List<String> getRemoteTessdataLanguages() {
return List.of("eng");
@@ -330,7 +332,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(missingDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected List<String> getRemoteTessdataLanguages() {
return List.of("eng");
@@ -352,7 +354,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected boolean isWritableDirectory(Path dir) {
return false;
@@ -21,8 +21,6 @@ import org.springframework.security.core.Authentication;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import com.fasterxml.jackson.databind.ObjectMapper;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.database.repository.UserRepository;
@@ -35,10 +33,13 @@ import stirling.software.proprietary.security.service.UserService;
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
import stirling.software.proprietary.service.UserLicenseSettingsService;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
@ExtendWith(MockitoExtension.class)
class UserControllerTest {
private final ObjectMapper objectMapper = new ObjectMapper();
private final ObjectMapper objectMapper = JsonMapper.builder().build();
@Mock private UserService userService;
@Mock private SessionPersistentRegistry sessionRegistry;
@@ -36,6 +36,9 @@ import stirling.software.proprietary.security.model.JwtVerificationKey;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.model.exception.AuthenticationFailureException;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
@ExtendWith(MockitoExtension.class)
class JwtServiceTest {
@@ -66,7 +69,8 @@ class JwtServiceTest {
testVerificationKey = new JwtVerificationKey("test-key-id", encodedPublicKey);
ApplicationProperties applicationProperties = new ApplicationProperties();
jwtService = new JwtService(true, keystoreService, applicationProperties);
ObjectMapper objectMapper = JsonMapper.builder().build();
jwtService = new JwtService(objectMapper, true, keystoreService, applicationProperties);
}
@Test