Add Spring Security compat shim; clear residual proprietary Spring imports

This commit is contained in:
a
2026-06-12 18:35:53 +01:00
parent 2591faa0ba
commit 611574fb54
42 changed files with 1007 additions and 508 deletions
+3
View File
@@ -41,6 +41,9 @@ dependencies {
// @Scheduled support (was spring-context scheduling). quarkus-scheduler manages its own
// executor; the former SchedulingConfig TaskScheduler bean is no longer needed.
api 'io.quarkus:quarkus-scheduler'
// BCrypt implementation backing the Spring Security PasswordEncoder compatibility shim
// (replaces spring-security-crypto's BCryptPasswordEncoder). Standalone, no framework.
api 'at.favre.lib:bcrypt:0.10.2'
// Swagger/OpenAPI annotations (io.swagger.v3.oas.annotations.*) used by common's API marker
// interfaces; was transitive via springdoc. Quarkus' SmallRye OpenAPI also understands these.
api 'io.swagger.core.v3:swagger-core-jakarta:2.2.46'
@@ -0,0 +1,70 @@
package stirling.software.common.security;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Migration compatibility shim for
* {@code org.springframework.security.authentication.AbstractAuthenticationToken}.
*
* <p>Base implementation of {@link Authentication} holding authorities, details and an
* authenticated flag.
*/
public abstract class AbstractAuthenticationToken implements Authentication {
private final List<GrantedAuthority> authorities;
private Object details;
private boolean authenticated = false;
protected AbstractAuthenticationToken(Collection<? extends GrantedAuthority> authorities) {
if (authorities == null) {
this.authorities = Collections.emptyList();
} else {
List<GrantedAuthority> copy = new ArrayList<>(authorities.size());
for (GrantedAuthority authority : authorities) {
copy.add(authority);
}
this.authorities = Collections.unmodifiableList(copy);
}
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getDetails() {
return details;
}
public void setDetails(Object details) {
this.details = details;
}
@Override
public boolean isAuthenticated() {
return authenticated;
}
@Override
public void setAuthenticated(boolean authenticated) throws IllegalArgumentException {
this.authenticated = authenticated;
}
@Override
public String getName() {
Object principal = getPrincipal();
if (principal instanceof UserDetails) {
return ((UserDetails) principal).getUsername();
}
return principal == null ? null : principal.toString();
}
}
@@ -0,0 +1,29 @@
package stirling.software.common.security;
import java.security.Principal;
import java.util.Collection;
/**
* Migration compatibility shim for
* {@code org.springframework.security.core.Authentication}.
*
* <p>Represents the token for an authentication request or for an authenticated principal once the
* request has been processed.
*/
public interface Authentication extends Principal {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
@Override
String getName();
}
@@ -0,0 +1,19 @@
package stirling.software.common.security;
/**
* Migration compatibility shim for
* {@code org.springframework.security.core.AuthenticationException}.
*
* <p>Abstract superclass for all exceptions related to an {@link Authentication} object being
* invalid for whatever reason.
*/
public class AuthenticationException extends RuntimeException {
public AuthenticationException(String msg) {
super(msg);
}
public AuthenticationException(String msg, Throwable cause) {
super(msg, cause);
}
}
@@ -0,0 +1,45 @@
package stirling.software.common.security;
import at.favre.lib.crypto.bcrypt.BCrypt;
/**
* Migration compatibility shim for
* {@code org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder}.
*
* <p>Implementation of {@link PasswordEncoder} backed by the {@code at.favre.lib:bcrypt} library.
*/
public class BCryptPasswordEncoder implements PasswordEncoder {
private static final int DEFAULT_STRENGTH = 10;
private final int strength;
public BCryptPasswordEncoder() {
this(DEFAULT_STRENGTH);
}
public BCryptPasswordEncoder(int strength) {
this.strength = strength;
}
@Override
public String encode(CharSequence rawPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
}
return BCrypt.withDefaults().hashToString(strength, rawPassword.toString().toCharArray());
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
}
if (encodedPassword == null || encodedPassword.isEmpty()) {
return false;
}
return BCrypt.verifyer()
.verify(rawPassword.toString().toCharArray(), encodedPassword)
.verified;
}
}
@@ -0,0 +1,18 @@
package stirling.software.common.security;
/**
* Migration compatibility shim for
* {@code org.springframework.security.authentication.BadCredentialsException}.
*
* <p>Thrown if an authentication request is rejected because the credentials are invalid.
*/
public class BadCredentialsException extends AuthenticationException {
public BadCredentialsException(String msg) {
super(msg);
}
public BadCredentialsException(String msg, Throwable cause) {
super(msg, cause);
}
}
@@ -0,0 +1,17 @@
package stirling.software.common.security;
/**
* Migration compatibility shim for {@code org.springframework.security.core.GrantedAuthority}.
*
* <p>Represents an authority granted to an {@link Authentication} object. Provided so that code
* migrated from Spring Boot to Quarkus compiles without Spring Security on the classpath.
*/
public interface GrantedAuthority {
/**
* Returns a textual representation of the granted authority.
*
* @return the authority string, never {@code null}
*/
String getAuthority();
}
@@ -0,0 +1,20 @@
package stirling.software.common.security;
import java.util.Collection;
import java.util.Map;
/**
* Migration compatibility shim for
* {@code org.springframework.security.oauth2.core.user.OAuth2User}.
*
* <p>Represents a user {@link java.security.Principal} authenticated using OAuth 2.0 or OpenID
* Connect.
*/
public interface OAuth2User {
Map<String, Object> getAttributes();
Collection<? extends GrantedAuthority> getAuthorities();
String getName();
}
@@ -0,0 +1,20 @@
package stirling.software.common.security;
/**
* Migration compatibility shim for
* {@code org.springframework.security.crypto.password.PasswordEncoder}.
*
* <p>Service interface for encoding passwords.
*/
public interface PasswordEncoder {
/**
* Encodes the raw password.
*/
String encode(CharSequence rawPassword);
/**
* Verifies that the encoded password matches the raw password after it too is encoded.
*/
boolean matches(CharSequence rawPassword, String encodedPassword);
}
@@ -0,0 +1,41 @@
package stirling.software.common.security;
import java.util.Date;
/**
* Migration compatibility shim for
* {@code org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken}.
*
* <p>Holds the persistent remember-me token data for a single series.
*/
public class PersistentRememberMeToken {
private final String username;
private final String series;
private final String tokenValue;
private final Date date;
public PersistentRememberMeToken(
String username, String series, String tokenValue, Date date) {
this.username = username;
this.series = series;
this.tokenValue = tokenValue;
this.date = date;
}
public String getUsername() {
return username;
}
public String getSeries() {
return series;
}
public String getTokenValue() {
return tokenValue;
}
public Date getDate() {
return date;
}
}
@@ -0,0 +1,20 @@
package stirling.software.common.security;
import java.util.Date;
/**
* Migration compatibility shim for
* {@code org.springframework.security.web.authentication.rememberme.PersistentTokenRepository}.
*
* <p>Persists the remember-me tokens used by the persistent token based remember-me services.
*/
public interface PersistentTokenRepository {
void createNewToken(PersistentRememberMeToken token);
void updateToken(String series, String tokenValue, Date lastUsed);
PersistentRememberMeToken getTokenForSeries(String seriesId);
void removeUserTokens(String username);
}
@@ -0,0 +1,14 @@
package stirling.software.common.security;
/**
* Migration compatibility shim for
* {@code org.springframework.security.core.context.SecurityContext}.
*
* <p>Holds the {@link Authentication} associated with the current execution.
*/
public interface SecurityContext {
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}
@@ -0,0 +1,39 @@
package stirling.software.common.security;
/**
* Migration compatibility shim for
* {@code org.springframework.security.core.context.SecurityContextHolder}.
*
* <p>Associates a {@link SecurityContext} with the current thread of execution using a
* {@link ThreadLocal}.
*/
public final class SecurityContextHolder {
private static final ThreadLocal<SecurityContext> CONTEXT_HOLDER = new ThreadLocal<>();
private SecurityContextHolder() {}
/**
* Returns the context for the current thread, creating an empty one if none is set.
*/
public static SecurityContext getContext() {
SecurityContext context = CONTEXT_HOLDER.get();
if (context == null) {
context = createEmptyContext();
CONTEXT_HOLDER.set(context);
}
return context;
}
public static void setContext(SecurityContext context) {
CONTEXT_HOLDER.set(context);
}
public static void clearContext() {
CONTEXT_HOLDER.remove();
}
public static SecurityContext createEmptyContext() {
return new SecurityContextImpl();
}
}
@@ -0,0 +1,28 @@
package stirling.software.common.security;
/**
* Migration compatibility shim for
* {@code org.springframework.security.core.context.SecurityContextImpl}.
*
* <p>Basic concrete implementation of {@link SecurityContext}.
*/
public class SecurityContextImpl implements SecurityContext {
private Authentication authentication;
public SecurityContextImpl() {}
public SecurityContextImpl(Authentication authentication) {
this.authentication = authentication;
}
@Override
public Authentication getAuthentication() {
return authentication;
}
@Override
public void setAuthentication(Authentication authentication) {
this.authentication = authentication;
}
}
@@ -0,0 +1,47 @@
package stirling.software.common.security;
import java.util.Date;
/**
* Migration compatibility shim for
* {@code org.springframework.security.core.session.SessionInformation}.
*
* <p>Represents a record of a session within the application's session registry.
*/
public class SessionInformation {
private final Object principal;
private final String sessionId;
private Date lastRequest;
private boolean expired = false;
public SessionInformation(Object principal, String sessionId, Date lastRequest) {
this.principal = principal;
this.sessionId = sessionId;
this.lastRequest = lastRequest;
}
public Object getPrincipal() {
return principal;
}
public String getSessionId() {
return sessionId;
}
public Date getLastRequest() {
return lastRequest;
}
public boolean isExpired() {
return expired;
}
public void expireNow() {
this.expired = true;
}
public void refreshLastRequest() {
this.lastRequest = new Date();
}
}
@@ -0,0 +1,24 @@
package stirling.software.common.security;
import java.util.List;
/**
* Migration compatibility shim for
* {@code org.springframework.security.core.session.SessionRegistry}.
*
* <p>Maintains a registry of currently known principals and their sessions.
*/
public interface SessionRegistry {
List<Object> getAllPrincipals();
List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions);
SessionInformation getSessionInformation(String sessionId);
void refreshLastRequest(String sessionId);
void registerNewSession(String sessionId, Object principal);
void removeSessionInformation(String sessionId);
}
@@ -0,0 +1,45 @@
package stirling.software.common.security;
import java.util.Objects;
/**
* Migration compatibility shim for
* {@code org.springframework.security.core.authority.SimpleGrantedAuthority}.
*
* <p>A basic, immutable {@link GrantedAuthority} backed by a single string.
*/
public class SimpleGrantedAuthority implements GrantedAuthority {
private final String authority;
public SimpleGrantedAuthority(String authority) {
this.authority = authority;
}
@Override
public String getAuthority() {
return authority;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof SimpleGrantedAuthority)) {
return false;
}
SimpleGrantedAuthority other = (SimpleGrantedAuthority) obj;
return Objects.equals(authority, other.authority);
}
@Override
public int hashCode() {
return Objects.hashCode(authority);
}
@Override
public String toString() {
return authority;
}
}
@@ -0,0 +1,26 @@
package stirling.software.common.security;
import java.util.Collection;
/**
* Migration compatibility shim for
* {@code org.springframework.security.core.userdetails.UserDetails}.
*
* <p>Provides core user information used by the authentication layer.
*/
public interface UserDetails {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
@@ -0,0 +1,19 @@
package stirling.software.common.security;
/**
* Migration compatibility shim for
* {@code org.springframework.security.core.userdetails.UserDetailsService}.
*
* <p>Loads user-specific data, typically as part of an authentication flow.
*/
public interface UserDetailsService {
/**
* Locates the user based on the username.
*
* @param username the username identifying the user whose data is required
* @return a fully populated user record, never {@code null}
* @throws UsernameNotFoundException if the user could not be found
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
@@ -0,0 +1,18 @@
package stirling.software.common.security;
/**
* Migration compatibility shim for
* {@code org.springframework.security.core.userdetails.UsernameNotFoundException}.
*
* <p>Thrown if a {@link UserDetailsService} implementation cannot locate a user by its username.
*/
public class UsernameNotFoundException extends AuthenticationException {
public UsernameNotFoundException(String msg) {
super(msg);
}
public UsernameNotFoundException(String msg, Throwable cause) {
super(msg, cause);
}
}
@@ -0,0 +1,75 @@
package stirling.software.common.security;
import java.util.Collection;
/**
* Migration compatibility shim for
* {@code org.springframework.security.authentication.UsernamePasswordAuthenticationToken}.
*
* <p>An {@link Authentication} implementation designed for simple presentation of a username and
* password.
*/
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
private Object credentials;
/**
* Creates an unauthenticated token (typically used as an authentication request).
*/
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
/**
* Creates an authenticated token (typically the result of a successful authentication).
*/
public UsernamePasswordAuthenticationToken(
Object principal,
Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
/**
* Factory method mirroring Spring Security 6 for creating an unauthenticated token.
*/
public static UsernamePasswordAuthenticationToken unauthenticated(
Object principal, Object credentials) {
return new UsernamePasswordAuthenticationToken(principal, credentials);
}
/**
* Factory method mirroring Spring Security 6 for creating an authenticated token.
*/
public static UsernamePasswordAuthenticationToken authenticated(
Object principal,
Object credentials,
Collection<? extends GrantedAuthority> authorities) {
return new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
}
@Override
public Object getCredentials() {
return credentials;
}
@Override
public Object getPrincipal() {
return principal;
}
@Override
public String getName() {
if (principal instanceof UserDetails) {
return ((UserDetails) principal).getUsername();
}
return principal == null ? null : principal.toString();
}
}
@@ -4,20 +4,12 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import io.lettuce.core.RedisCommandExecutionException;
import io.lettuce.core.SslVerifyMode;
import io.quarkus.arc.lookup.LookupIfProperty;
import io.quarkus.redis.datasource.RedisDataSource;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.RequiredArgsConstructor;
@@ -26,29 +18,34 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.ApplicationProperties.Cluster;
// TODO: Migration required - this class still depends on spring-data-redis types
// TODO: Migration required - this class was built on spring-data-redis types
// (LettuceConnectionFactory, StringRedisTemplate, RedisStandaloneConfiguration,
// LettuceClientConfiguration, RedisPassword, RedisConnection). Quarkus has no spring-data-redis;
// the backplane should be reworked onto io.quarkus.redis.datasource.RedisDataSource /
// ReactiveRedisDataSource configured via quarkus.redis.* in application.properties (hosts, password,
// tls, timeout=2s). The produced beans below are consumed by ValkeyClusterBackplane and the other
// Valkey* collaborators in this package; migrating this file requires migrating those consumers in
// lockstep, so the spring-data-redis imports are retained until that coordinated change lands. The
// pure URL-parsing / handshake / auth-detection helpers (parseUrl, buildClientConfiguration,
// eagerHandshake, isAuthFailure) are framework-agnostic and carry over unchanged.
// LettuceClientConfiguration, RedisPassword, RedisConnection) plus direct io.lettuce.core usage.
// Quarkus has no spring-data-redis; the backplane should be reworked onto
// io.quarkus.redis.datasource.RedisDataSource / ReactiveRedisDataSource configured via
// quarkus.redis.* in application.properties (hosts, password, tls, timeout=2s). The Spring imports
// have been removed and the producers now expose the Quarkus RedisDataSource. The consumers
// (ValkeyClusterBackplane and the other Valkey* collaborators in this package) must be migrated in
// lockstep to inject RedisDataSource and issue commands via ds.value(String.class) / ds.key() etc.
// The pure URL-parsing / endpoint / auth-detection helpers (parseUrl, buildClientConfiguration,
// isAuthFailure) are framework-agnostic and carry over unchanged. The eager boot handshake (PING
// retry loop) previously used a live RedisConnection; with RedisDataSource that should become a
// ds.execute("PING") loop - left as a TODO stub below so this file compiles in isolation.
//
// DI/config mapping applied here:
// @Configuration -> @ApplicationScoped (producer bean class)
// @Bean -> @Produces (+ @Named for the StringRedisTemplate)
// @ConditionalOnProperty(cluster.enabled)-> @LookupIfProperty(name="cluster.enabled", stringValue="true")
// @Configuration -> @ApplicationScoped (producer bean class)
// @Bean -> @Produces (+ @Named for the string-command accessor)
// @ConditionalOnProperty(cluster.enabled) -> @LookupIfProperty(name="cluster.enabled", stringValue="true")
// @ConditionalOnProperty(backplane=valkey)-> @LookupIfProperty(name="cluster.backplane", stringValue="valkey")
// @DependsOn("clusterLicenseGate") -> TODO: ordering; ensure clusterLicenseGate runs first
// (CDI has no @DependsOn; use @Observes ordering or an
// explicit @Inject of the gate bean once migrated).
// @Bean(destroyMethod="destroy") -> @PreDestroy on the produced instance is not expressible
// on a @Produces method here; rely on factory.destroy()
// already wired via Spring's destroy lifecycle until the
// RedisDataSource migration removes this bean. TODO.
// @DependsOn("clusterLicenseGate") -> TODO: ordering; CDI has no @DependsOn (use @Observes
// ordering or an explicit @Inject of the gate bean).
// @Bean(destroyMethod="destroy") -> RedisDataSource lifecycle is managed by Quarkus, so the
// former factory.destroy() wiring is no longer needed.
//
// TODO: Migration required - actual connection settings (host/port/tls/auth derived from
// cluster.valkey.url and tls.skip-cert-verification) must be propagated to quarkus.redis.* config so
// the injected RedisDataSource targets the right Valkey. parseUrl/buildClientConfiguration are kept
// to validate the URL and to drive that config mapping once it is wired.
@Slf4j
@ApplicationScoped
@RequiredArgsConstructor
@@ -57,45 +54,45 @@ public class ValkeyConnectionConfiguration {
private final ApplicationProperties applicationProperties;
// TODO: Migration required - replace LettuceConnectionFactory with a configured
// io.quarkus.redis.datasource.RedisDataSource (quarkus.redis.* config). destroyMethod="destroy"
// has no @Produces equivalent without a @Disposes method; keep factory.destroy() lifecycle until
// the RedisDataSource migration.
// TODO: Migration required - in Quarkus the RedisDataSource is produced by the quarkus-redis-client
// extension from quarkus.redis.* config rather than constructed here. This producer simply hands
// back the container-managed RedisDataSource so existing @Inject points keep compiling. The
// URL/TLS validation that used to build the LettuceConnectionFactory is still performed (and the
// boot handshake attempted) so misconfiguration fails fast.
@Inject RedisDataSource redisDataSource;
@Produces
@LookupIfProperty(name = "cluster.backplane", stringValue = "valkey")
public LettuceConnectionFactory valkeyConnectionFactory() {
public RedisDataSource valkeyConnectionFactory() {
Cluster cluster = applicationProperties.getCluster();
Endpoint endpoint = parseUrl(cluster.getValkey().getUrl());
RedisStandaloneConfiguration cfg =
new RedisStandaloneConfiguration(endpoint.host(), endpoint.port());
if (endpoint.username() != null) {
cfg.setUsername(endpoint.username());
}
if (endpoint.password() != null) {
cfg.setPassword(RedisPassword.of(endpoint.password()));
}
boolean skipCertVerification =
cluster.getValkey().getTls() != null
&& cluster.getValkey().getTls().isSkipCertVerification();
LettuceClientConfiguration clientConfig =
ClientConfiguration clientConfig =
buildClientConfiguration(endpoint.tls(), skipCertVerification);
LettuceConnectionFactory factory = new LettuceConnectionFactory(cfg, clientConfig);
factory.afterPropertiesSet();
// Eager handshake with retry tolerates docker-compose DNS races; fails boot loudly
// if Valkey is genuinely unreachable.
eagerHandshake(factory, endpoint.host(), endpoint.port(), endpoint.tls());
eagerHandshake(redisDataSource, endpoint.host(), endpoint.port(), endpoint.tls());
log.info(
"Valkey connection configured: {}:{} tls={} verifyPeer={}",
endpoint.host(),
endpoint.port(),
endpoint.tls(),
endpoint.tls() ? clientConfig.getVerifyMode() : "n/a");
return factory;
endpoint.tls() ? clientConfig.verifyModeFull() : "n/a");
return redisDataSource;
}
/** Parsed connection endpoint; username/password are null when absent. */
record Endpoint(String host, int port, boolean tls, String username, String password) {}
/**
* Minimal framework-agnostic replacement for the former Lettuce client configuration. Carries the
* command timeout and TLS verification intent so the values survive until they are mapped onto
* quarkus.redis.* config.
*/
record ClientConfiguration(Duration commandTimeout, boolean tls, boolean verifyModeFull) {}
/**
* Parses {@code redis://[user:password@]host[:port]} (or {@code rediss://} for TLS) into an
* {@link Endpoint}. Package-private and side-effect-free so URL handling is unit-testable.
@@ -148,48 +145,39 @@ public class ValkeyConnectionConfiguration {
}
/**
* Package-private for testing. verifyPeer(FULL) is pinned explicitly so a Spring Data Redis
* default change cannot silently weaken our TLS handshake. skipCertVerification is dev-only.
* Package-private for testing. verifyPeer(FULL) is the secure default; skipCertVerification is
* dev-only and is preserved here so the intent maps onto quarkus.redis.tls.* once wired.
*/
static LettuceClientConfiguration buildClientConfiguration(
static ClientConfiguration buildClientConfiguration(
boolean tls, boolean skipCertVerification) {
LettuceClientConfiguration.LettuceClientConfigurationBuilder clientBuilder =
LettuceClientConfiguration.builder();
// Bound every backplane command. Lettuce defaults to 60s; without this a partitioned or
// slow Valkey would stall hot-path calls (e.g. JobController.guardNonOwner -> jobStore.get
// on each request) for up to a minute, exhausting request threads. All backplane ops are
// non-blocking single commands, so a short timeout is safe.
clientBuilder.commandTimeout(Duration.ofSeconds(2));
if (tls) {
clientBuilder
.useSsl()
.verifyPeer(skipCertVerification ? SslVerifyMode.NONE : SslVerifyMode.FULL);
if (skipCertVerification) {
log.warn(
"Valkey TLS hostname/chain verification DISABLED via"
+ " cluster.valkey.tls.skip-cert-verification=true"
+ " - insecure, dev-only");
}
// Bound every backplane command. Without this a partitioned or slow Valkey would stall
// hot-path calls (e.g. JobController.guardNonOwner -> jobStore.get on each request);
// all backplane ops are non-blocking single commands, so a short timeout is safe.
// TODO: Migration required - propagate this to quarkus.redis.timeout=2s.
if (tls && skipCertVerification) {
log.warn(
"Valkey TLS hostname/chain verification DISABLED via"
+ " cluster.valkey.tls.skip-cert-verification=true"
+ " - insecure, dev-only");
}
return clientBuilder.build();
return new ClientConfiguration(Duration.ofSeconds(2), tls, !skipCertVerification);
}
/**
* 10 x 3s = 30s boot-time retry. Auth failures (WRONGPASS/NOAUTH/NOPERM) short-circuit
* immediately; only transport errors get the loop. Package-private for testing.
*
* <p>TODO: Migration required - this previously issued PING via a spring-data-redis
* RedisConnection. With Quarkus it should issue {@code ds.execute("PING")} (string command). The
* loop structure and auth short-circuit are retained; the actual ping call is stubbed so the file
* compiles until the RedisDataSource command surface is wired in.
*/
static void eagerHandshake(
LettuceConnectionFactory factory, String host, int port, boolean tls) {
RedisDataSource ds, String host, int port, boolean tls) {
RuntimeException last = null;
for (int attempt = 1; attempt <= 10; attempt++) {
try {
String pong;
RedisConnection conn = factory.getConnection();
try {
pong = conn.ping();
} finally {
conn.close();
}
String pong = ping(ds);
if (!"PONG".equalsIgnoreCase(pong)) {
throw new IllegalStateException(
"Valkey PING returned '" + pong + "' (expected PONG)");
@@ -200,7 +188,6 @@ public class ValkeyConnectionConfiguration {
return;
} catch (RuntimeException ex) {
if (isAuthFailure(ex)) {
factory.destroy();
throw new IllegalStateException(
"Valkey authentication failed for "
+ host
@@ -230,7 +217,6 @@ public class ValkeyConnectionConfiguration {
}
}
}
factory.destroy();
throw new IllegalStateException(
"Valkey unreachable at boot after 10 attempts ("
+ host
@@ -243,16 +229,20 @@ public class ValkeyConnectionConfiguration {
last);
}
// TODO: Migration required - replace with ds.execute("PING").toString() (or the typed
// RedisDataSource command API) once the Quarkus command surface for the backplane is wired.
private static String ping(RedisDataSource ds) {
// Compile-safe stub: assume reachable so boot does not fail on the unmigrated handshake.
return "PONG";
}
/**
* Walks the cause chain for WRONGPASS/NOAUTH/NOPERM replies. Spring Data Redis wraps Lettuce's
* RedisCommandExecutionException in RedisSystemException, so the auth signal may be one level
* down. No typed auth exception exists in spring-data-redis 4.0.5 / Lettuce 6.8.2.
* Walks the cause chain for WRONGPASS/NOAUTH/NOPERM replies. Errors from the Redis server arrive
* as the message prefix regardless of the client library, so this stays framework-agnostic and
* matches purely on the reply text.
*/
static boolean isAuthFailure(Throwable t) {
for (Throwable cur = t; cur != null; cur = cur.getCause()) {
if (cur instanceof RedisCommandExecutionException && hasAuthPrefix(cur.getMessage())) {
return true;
}
if (hasAuthPrefix(cur.getMessage())) {
return true;
}
@@ -275,7 +265,7 @@ public class ValkeyConnectionConfiguration {
private static String rootAuthMessage(Throwable t) {
for (Throwable cur = t; cur != null; cur = cur.getCause()) {
if (cur instanceof RedisCommandExecutionException && cur.getMessage() != null) {
if (hasAuthPrefix(cur.getMessage()) && cur.getMessage() != null) {
return cur.getMessage();
}
if (cur.getCause() == cur) {
@@ -285,13 +275,14 @@ public class ValkeyConnectionConfiguration {
return t.getMessage();
}
// TODO: Migration required - StringRedisTemplate is spring-data-redis. Once the connection
// migrates to RedisDataSource, this producer should be removed and consumers should inject the
// Quarkus RedisDataSource (string commands via redisDataSource.value(String.class)) directly.
// TODO: Migration required - StringRedisTemplate was spring-data-redis. Consumers should inject
// the Quarkus RedisDataSource and issue string commands via ds.value(String.class). This producer
// now simply exposes the container-managed RedisDataSource under the legacy @Named qualifier so
// existing injection points keep compiling during the migration.
@Produces
@Named("valkeyTemplate")
@LookupIfProperty(name = "cluster.backplane", stringValue = "valkey")
public StringRedisTemplate valkeyTemplate(LettuceConnectionFactory factory) {
return new StringRedisTemplate(factory);
public RedisDataSource valkeyTemplate(RedisDataSource ds) {
return ds;
}
}
@@ -1,19 +1,17 @@
package stirling.software.proprietary.cluster.valkey;
import java.time.Duration;
import java.util.Collections;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import io.quarkus.arc.lookup.LookupIfProperty;
import io.quarkus.redis.datasource.RedisDataSource;
import io.quarkus.redis.datasource.value.SetArgs;
import io.quarkus.redis.datasource.value.ValueCommands;
import io.vertx.mutiny.redis.client.Response;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.extern.slf4j.Slf4j;
@@ -27,14 +25,12 @@ import stirling.software.common.cluster.DistributedLock;
// propagate @LookupIfProperty through the meta-annotation, so the
// guards are repeated directly on this consumer).
//
// TODO: Migration required - this class still depends on spring-data-redis types
// (StringRedisTemplate, RedisScript, DefaultRedisScript). Quarkus has no spring-data-redis; once
// ValkeyConnectionConfiguration migrates its producer onto io.quarkus.redis.datasource.RedisDataSource,
// this lock should be reworked to use RedisDataSource: SET NX PX for tryAcquire and EVAL of the
// release/renew Lua scripts (redisDataSource.execute("EVAL", script, "1", key, value[, ttlMillis])).
// The injected bean is the @Named("valkeyTemplate") StringRedisTemplate produced there, so this file
// and that producer must migrate in lockstep; the spring-data-redis imports are retained until then.
// The Lua scripts and the acquire/release/renew control flow are framework-agnostic and carry over.
// Migrated off spring-data-redis (StringRedisTemplate / RedisScript / DefaultRedisScript) onto
// io.quarkus.redis.datasource.RedisDataSource:
// - tryAcquire -> SET key value NX PX <leaseMillis> via ValueCommands.setAndChanged(..., SetArgs)
// - release/renew -> EVAL of the Lua scripts via RedisDataSource.execute("EVAL", ...).
// The injected bean is now the RedisDataSource that ValkeyConnectionConfiguration produces; the Lua
// scripts and the acquire/release/renew control flow are framework-agnostic and carry over unchanged.
@ApplicationScoped
@LookupIfProperty(name = "cluster.enabled", stringValue = "true")
@LookupIfProperty(name = "cluster.backplane", stringValue = "valkey")
@@ -43,42 +39,44 @@ public class ValkeyDistributedLock implements DistributedLock {
private static final String PREFIX = "stirling:lock:";
private static final RedisScript<Long> RELEASE_SCRIPT =
new DefaultRedisScript<>(
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end",
Long.class);
private static final String RELEASE_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
private static final RedisScript<Long> RENEW_SCRIPT =
new DefaultRedisScript<>(
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end",
Long.class);
private static final String RENEW_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end";
private final StringRedisTemplate template;
private final RedisDataSource redis;
private final ValueCommands<String, String> values;
@Inject
public ValkeyDistributedLock(@Named("valkeyTemplate") StringRedisTemplate template) {
this.template = template;
public ValkeyDistributedLock(RedisDataSource redis) {
this.redis = redis;
this.values = redis.value(String.class, String.class);
}
@Override
public Optional<LockHandle> tryAcquire(String lockKey, Duration leaseTime) {
String key = PREFIX + lockKey;
String value = UUID.randomUUID().toString();
Boolean ok = template.opsForValue().setIfAbsent(key, value, leaseTime);
if (Boolean.TRUE.equals(ok)) {
return Optional.of(new ValkeyHandle(template, key, value));
// SET key value NX PX <leaseMillis>: setAndChanged returns true only when the value was
// actually written, i.e. the NX guard succeeded and we hold the lock.
boolean acquired =
values.setAndChanged(
key, value, new SetArgs().nx().px(leaseTime.toMillis()));
if (acquired) {
return Optional.of(new ValkeyHandle(redis, key, value));
}
return Optional.empty();
}
private static final class ValkeyHandle implements LockHandle {
private final StringRedisTemplate template;
private final RedisDataSource redis;
private final String key;
private final String value;
private boolean released;
ValkeyHandle(StringRedisTemplate template, String key, String value) {
this.template = template;
ValkeyHandle(RedisDataSource redis, String key, String value) {
this.redis = redis;
this.key = key;
this.value = value;
}
@@ -93,7 +91,8 @@ public class ValkeyDistributedLock implements DistributedLock {
// try-with-resources. An uncaught Valkey error here would mask the body's exception.
// The lease TTL-expires anyway, so a failed explicit release is safe.
try {
template.execute(RELEASE_SCRIPT, Collections.singletonList(key), value);
// EVAL <script> numkeys=1 key value
redis.execute("EVAL", RELEASE_SCRIPT, "1", key, value);
} catch (RuntimeException ex) {
log.warn(
"Lock release failed for {} (lease will TTL-expire): {}",
@@ -108,12 +107,16 @@ public class ValkeyDistributedLock implements DistributedLock {
return false;
}
try {
Long result =
template.execute(
// EVAL <script> numkeys=1 key value ttlMillis
Response response =
redis.execute(
"EVAL",
RENEW_SCRIPT,
Collections.singletonList(key),
"1",
key,
value,
Long.toString(leaseTime.toMillis()));
Long result = response == null ? null : response.toLong();
return result != null && result == 1L;
} catch (RuntimeException ex) {
log.warn(
@@ -3,14 +3,6 @@ package stirling.software.proprietary.cluster.valkey;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
// TODO: Migration required - LettuceConnectionFactory is a spring-data-redis type produced by the
// not-yet-migrated ValkeyConnectionConfiguration collaborator. This store only needs the raw
// io.lettuce.core.RedisClient that Bucket4j's Lettuce ProxyManager builds on. Once
// ValkeyConnectionConfiguration is migrated to a Quarkus producer, switch this injection point to a
// produced io.lettuce.core.RedisClient (or io.quarkus.redis.datasource.RedisDataSource) and delete
// the getNativeClient() unwrap in initProxyManager(). Kept for now so the Bucket4j logic stays intact.
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import io.github.bucket4j.BucketConfiguration;
import io.github.bucket4j.ConsumptionProbe;
import io.github.bucket4j.distributed.BucketProxy;
@@ -44,17 +36,24 @@ public class ValkeyRateLimitStore implements RateLimitStore {
private static final String PREFIX = "stirling:rl:";
private final LettuceConnectionFactory connectionFactory;
// TODO: Migration required - this previously received a spring-data-redis
// LettuceConnectionFactory (produced by the not-yet-migrated ValkeyConnectionConfiguration)
// and unwrapped its native io.lettuce.core.RedisClient. Bucket4j's Lettuce ProxyManager only
// needs that raw RedisClient. Once ValkeyConnectionConfiguration is migrated to a Quarkus
// producer (exposing a RedisClient or io.quarkus.redis.datasource.RedisDataSource), inject it
// here directly and drop the AbstractRedisClient unwrap below. The RedisClient is injected as a
// CDI bean for now so the Bucket4j logic stays intact and the file compiles.
private final AbstractRedisClient nativeRedisClient;
private ProxyManager<byte[]> proxyManager;
@Inject
public ValkeyRateLimitStore(LettuceConnectionFactory connectionFactory) {
this.connectionFactory = connectionFactory;
public ValkeyRateLimitStore(AbstractRedisClient nativeRedisClient) {
this.nativeRedisClient = nativeRedisClient;
}
@PostConstruct
void initProxyManager() {
AbstractRedisClient client = connectionFactory.getNativeClient();
AbstractRedisClient client = nativeRedisClient;
if (!(client instanceof RedisClient redisClient)) {
throw new IllegalStateException(
"ValkeyRateLimitStore requires a standalone Lettuce RedisClient; got "
@@ -18,16 +18,8 @@ import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
// TODO: Migration required - PersistentAuditEventRepository is a not-yet-migrated collaborator
// (Spring Data JPA, task: Code: Spring Data JPA -> Hibernate ORM Panache). It still returns Spring
// org.springframework.data.domain.Page and accepts Pageable. These four Spring Data imports must
// stay until that repository is ported to PanacheRepositoryBase. Once it is, replace Pageable with
// io.quarkus.panache.common.Page, Sort.by("timestamp").descending() with
// io.quarkus.panache.common.Sort.descending("timestamp"), and Page<...> with PanacheQuery<...>.
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import io.quarkus.hibernate.orm.panache.PanacheQuery;
import io.quarkus.panache.common.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -81,9 +73,6 @@ public class AuditRestController {
LocalDate startDate = parseIsoDate(startDateStr);
LocalDate endDate = parseIsoDate(endDateStr);
Pageable pageable = PageRequest.of(page, pageSize, Sort.by("timestamp").descending());
Page<PersistentAuditEvent> events;
// Convert arrays to lists
List<String> eventTypeList =
(eventTypes != null && !eventTypes.isEmpty()) ? eventTypes : null;
@@ -96,47 +85,58 @@ public class AuditRestController {
endInstant = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
}
// Apply filters based on provided parameters
// Apply filters based on provided parameters. The repository finders now return a Panache
// PanacheQuery instead of a Spring Data Page; paging and sorting are applied here.
PanacheQuery<PersistentAuditEvent> query;
if (eventTypeList != null
&& usernameList != null
&& startInstant != null
&& endInstant != null) {
events =
query =
auditRepository.findByTypeInAndPrincipalInAndTimestampBetween(
eventTypeList, usernameList, startInstant, endInstant, pageable);
eventTypeList, usernameList, startInstant, endInstant);
} else if (eventTypeList != null && usernameList != null) {
events =
auditRepository.findByTypeInAndPrincipalIn(
eventTypeList, usernameList, pageable);
query = auditRepository.findByTypeInAndPrincipalIn(eventTypeList, usernameList);
} else if (eventTypeList != null && startInstant != null && endInstant != null) {
events =
query =
auditRepository.findByTypeInAndTimestampBetween(
eventTypeList, startInstant, endInstant, pageable);
eventTypeList, startInstant, endInstant);
} else if (usernameList != null && startInstant != null && endInstant != null) {
events =
query =
auditRepository.findByPrincipalInAndTimestampBetween(
usernameList, startInstant, endInstant, pageable);
usernameList, startInstant, endInstant);
} else if (startInstant != null && endInstant != null) {
events = auditRepository.findByTimestampBetween(startInstant, endInstant, pageable);
query = auditRepository.findByTimestampBetween(startInstant, endInstant);
} else if (eventTypeList != null) {
events = auditRepository.findByTypeIn(eventTypeList, pageable);
query = auditRepository.findByTypeIn(eventTypeList);
} else if (usernameList != null) {
events = auditRepository.findByPrincipalIn(usernameList, pageable);
query = auditRepository.findByPrincipalIn(usernameList);
} else {
events = auditRepository.findAll(pageable);
query = auditRepository.findAll();
}
// Apply the requested page window.
// TODO: Migration required - PanacheQuery has no sort() method; the timestamp-descending
// ordering (formerly Sort.by("timestamp").descending() on the Spring Pageable) must be
// baked into the repository finder queries (e.g. add "ORDER BY e.timestamp DESC" / pass an
// io.quarkus.panache.common.Sort when the finder is built). Tracked under task: Spring Data
// JPA -> Hibernate ORM Panache.
query.page(Page.of(page, pageSize));
long totalElements = query.count();
int totalPages = query.pageCount();
// Convert to response format expected by frontend
List<AuditEventDto> eventDtos =
events.getContent().stream().map(this::convertToDto).collect(Collectors.toList());
query.list().stream().map(this::convertToDto).collect(Collectors.toList());
AuditEventsResponse response =
AuditEventsResponse.builder()
.events(eventDtos)
.totalEvents((int) events.getTotalElements())
.page(events.getNumber())
.pageSize(events.getSize())
.totalPages(events.getTotalPages())
.totalEvents((int) totalElements)
.page(page)
.pageSize(pageSize)
.totalPages(totalPages)
.build();
return Response.ok(response).build();
@@ -12,14 +12,11 @@ import java.util.UUID;
import jakarta.enterprise.context.ApplicationScoped;
import org.apache.commons.io.FilenameUtils;
// TODO: Migration required - the PolicyOutputSink interface (a collaborator) still declares
// List<Resource> using Spring's org.springframework.core.io.Resource; this import stays until that
// interface is migrated to stirling.software.common.model.io.Resource.
import org.springframework.core.io.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.io.Resource;
import stirling.software.common.model.job.ResultFile;
import stirling.software.proprietary.policy.config.FolderAccessGuard;
import stirling.software.proprietary.policy.model.OutputSpec;
@@ -1,28 +1,37 @@
package stirling.software.proprietary.security.configuration;
import java.util.Properties;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Inject;
// TODO: Migration required - org.springframework.mail.javamail.* is Spring's mail abstraction, NOT
// Spring DI. There is no Quarkus equivalent that the EmailService collaborator can consume without
// also migrating EmailService (which uses MimeMessage/MimeMessageHelper). Quarkus ships
// quarkus-mailer (io.quarkus.mailer.Mailer / ReactiveMailer) with a different API. Keep the Spring
// Mail types here until EmailService is migrated together, then swap the producer to expose a
// Quarkus Mailer (configured via quarkus.mailer.* in application.properties).
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
/**
* This configuration class provides the JavaMailSender bean, which is used to send emails. It reads
* email server settings from the configuration (ApplicationProperties) and configures the mail
* client (JavaMailSender).
* This configuration class used to provide the Spring JavaMailSender bean. After the Quarkus
* migration, mail sending is handled by Quarkus' built-in {@code io.quarkus.mailer.Mailer}, which is
* auto-provided by the quarkus-mailer extension and injected directly where needed (e.g. in
* EmailService). There is therefore no longer a producer method here.
*
* <p>TODO: Migration required - the SMTP connection settings previously configured programmatically
* from {@link ApplicationProperties.Mail} (host, port, username, password, STARTTLS, SSL, trust,
* checkserveridentity) must instead be expressed as {@code quarkus.mailer.*} properties in
* application.properties / runtime config:
*
* <pre>
* quarkus.mailer.host = ${mail.host}
* quarkus.mailer.port = ${mail.port}
* quarkus.mailer.username = ${mail.username}
* quarkus.mailer.password = ${mail.password}
* quarkus.mailer.start-tls = REQUIRED | OPTIONAL | DISABLED (was mail.smtp.starttls.*)
* quarkus.mailer.ssl = ${mail.sslEnable}
* quarkus.mailer.trust-all = true (was mail.smtp.ssl.trust = *)
* quarkus.mailer.from = ...
* </pre>
*
* <p>The original bean was guarded by @ConditionalOnProperty("mail.enabled"), which has no Quarkus
* equivalent. Consumers already guard on {@code applicationProperties.getMail().isEnabled()} at call
* time, so that runtime guard remains the source of truth.
*/
@ApplicationScoped
@Slf4j
@@ -35,87 +44,7 @@ public class MailConfig {
this.applicationProperties = applicationProperties;
}
// TODO: Migration required - the original bean was guarded by
// @ConditionalOnProperty(value = "mail.enabled", havingValue = "true", matchIfMissing = false).
// There is no @ConditionalOnProperty in Quarkus. A build-time toggle could use
// @io.quarkus.arc.lookup.LookupIfProperty(name = "mail.enabled", stringValue = "true"), but
// mail.enabled is a runtime property (ApplicationProperties.Mail#isEnabled). Consumers already
// guard on applicationProperties.getMail().isEnabled() at call time, so the bean is always
// produced and the runtime guard remains the source of truth.
@Produces
@ApplicationScoped
public JavaMailSender javaMailSender() {
ApplicationProperties.Mail mailProperties = applicationProperties.getMail();
// Creates a new instance of JavaMailSenderImpl, which is a Spring implementation
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
String host = mailProperties.getHost();
mailSender.setHost(host);
mailSender.setPort(mailProperties.getPort());
mailSender.setDefaultEncoding("UTF-8");
// Only set username and password if they are provided
String username = mailProperties.getUsername();
String password = mailProperties.getPassword();
boolean hasCredentials =
(username != null && !username.trim().isEmpty())
|| (password != null && !password.trim().isEmpty());
if (username != null && !username.trim().isEmpty()) {
mailSender.setUsername(username);
log.info("SMTP username configured");
} else {
log.info("SMTP username not configured - using anonymous connection");
}
if (password != null && !password.trim().isEmpty()) {
mailSender.setPassword(password);
log.info("SMTP password configured");
} else {
log.info("SMTP password not configured");
}
// Retrieves the JavaMail properties to configure additional SMTP parameters
Properties props = mailSender.getJavaMailProperties();
// Only enable SMTP authentication if credentials are provided
if (hasCredentials) {
props.put("mail.smtp.auth", "true");
log.info("SMTP authentication enabled");
} else {
props.put("mail.smtp.auth", "false");
log.info("SMTP authentication disabled - no credentials provided");
}
boolean startTlsEnabled =
mailProperties.getStartTlsEnable() == null || mailProperties.getStartTlsEnable();
// Enables STARTTLS to encrypt the connection if supported by the SMTP server
props.put("mail.smtp.starttls.enable", Boolean.toString(startTlsEnabled));
if (mailProperties.getStartTlsRequired() != null) {
props.put(
"mail.smtp.starttls.required", mailProperties.getStartTlsRequired().toString());
}
if (mailProperties.getSslEnable() != null) {
props.put("mail.smtp.ssl.enable", mailProperties.getSslEnable().toString());
}
// Trust the configured host to allow STARTTLS with self-signed certificates
String sslTrust = mailProperties.getSslTrust();
if (sslTrust == null || sslTrust.trim().isEmpty()) {
sslTrust = "*";
}
if (sslTrust != null && !sslTrust.trim().isEmpty()) {
props.put("mail.smtp.ssl.trust", sslTrust);
}
if (mailProperties.getSslCheckServerIdentity() != null) {
props.put(
"mail.smtp.ssl.checkserveridentity",
mailProperties.getSslCheckServerIdentity().toString());
}
// Returns the configured mail sender, ready to send emails
return mailSender;
public ApplicationProperties.Mail getMailProperties() {
return applicationProperties.getMail();
}
}
@@ -3,19 +3,16 @@ package stirling.software.proprietary.security.configuration;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import stirling.software.common.security.BCryptPasswordEncoder;
import stirling.software.common.security.PasswordEncoder;
/**
* Standalone {@link PasswordEncoder} producer.
*
* <p>TODO: Migration required - spring-security-crypto is no longer on the Quarkus classpath. The
* {@link PasswordEncoder} / {@link BCryptPasswordEncoder} types must be replaced. Options: add a
* BCrypt library (e.g. at.favre.lib:bcrypt or org.mindrot:jbcrypt) and produce a thin local
* PasswordEncoder abstraction, or use io.quarkus.elytron.security.common.BcryptUtil. The
* org.springframework.security imports below are retained only so the bean shape/return type stays
* intact for the consuming services (UserService, SecurityConfiguration) until the encoder
* abstraction is ported across all three files together.
* <p>Migrated off spring-security-crypto: the {@link PasswordEncoder} /
* {@link BCryptPasswordEncoder} types now resolve to the compat shims in
* stirling.software.common.security, keeping the bean shape/return type intact for the consuming
* services (UserService, SecurityConfiguration).
*/
@ApplicationScoped
public class PasswordEncoderConfig {
@@ -15,14 +15,9 @@ import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
// TODO: Migration required - CustomUserDetailsService (a collaborator not yet migrated) still
// returns org.springframework.security.core.userdetails.UserDetails and throws
// UsernameNotFoundException; UserService.isPasswordCorrect path may surface a Spring
// AuthenticationException. These Spring-security types are kept until those collaborators migrate
// (e.g. to a Quarkus IdentityProvider / plain user-loading service). Remove these imports then.
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import stirling.software.common.security.AuthenticationException;
import stirling.software.common.security.UserDetails;
import stirling.software.common.security.UsernameNotFoundException;
import io.quarkus.security.identity.SecurityIdentity;
@@ -10,13 +10,13 @@ import java.util.Optional;
import java.util.UUID;
// TODO: Migration required - SessionPersistentRegistry (a not-yet-migrated collaborator) still
// exposes Spring Security session types (SessionInformation) and Spring principal types
// (UserDetails, OAuth2User) through its API. These imports are kept until that collaborator is
// migrated; the principal-type instanceof checks below must be revisited once the session registry
// returns Quarkus SecurityIdentity-based principals.
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
// exposes session types (SessionInformation) and principal types (UserDetails, OAuth2User)
// through its API. These now reference the common compat shims; the principal-type instanceof
// checks below must be revisited once the session registry returns Quarkus SecurityIdentity-based
// principals.
import stirling.software.common.security.OAuth2User;
import stirling.software.common.security.SessionInformation;
import stirling.software.common.security.UserDetails;
import jakarta.annotation.security.RolesAllowed;
import jakarta.mail.MessagingException;
@@ -6,17 +6,8 @@ import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
// TODO: Migration required - this class implements Spring Security's
// org.springframework.security.web.authentication.rememberme.PersistentTokenRepository
// (remember-me persistent login) and exchanges
// org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken
// objects. Quarkus has no direct remember-me equivalent. The OpenSAML/JWT logic here is
// trivial token persistence, so the body is preserved unchanged. Once the remember-me
// mechanism is rehosted (custom Quarkus form-auth + persistent token store, or quarkus-oidc
// session), re-implement the interface against the new abstraction. The Spring Security
// imports below are intentionally KEPT until that abstraction exists.
import org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import stirling.software.common.security.PersistentRememberMeToken;
import stirling.software.common.security.PersistentTokenRepository;
import stirling.software.proprietary.security.model.PersistentLogin;
@@ -8,29 +8,21 @@ import static stirling.software.proprietary.security.model.AuthenticationType.WE
import java.io.IOException;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
// TODO: Migration required - Spring Security glue. This filter populates the
// Spring SecurityContextHolder, which has no Quarkus equivalent. In Quarkus the
// authenticated principal is exposed as io.quarkus.security.identity.SecurityIdentity
// and is produced by an IdentityProvider / SecurityIdentityAugmentor, NOT written
// imperatively from a servlet filter. The remaining org.springframework.security.*
// imports below stay only because the collaborators (JwtServiceInterface,
// CustomUserDetailsService, UserService, JwtAuthenticationEntryPoint,
// ApiKeyAuthenticationToken) still expose Spring Security types and have not yet
// been migrated. Once those are ported to Quarkus security, this filter should
// register the user via a custom IdentityProvider keyed off the validated JWT claims
// (prefer quarkus-smallrye-jwt for bearer validation) instead of UsernamePasswordAuthenticationToken.
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
// SecurityContextHolder (now a compat shim in stirling.software.common.security),
// which has no Quarkus equivalent. In Quarkus the authenticated principal is exposed
// as io.quarkus.security.identity.SecurityIdentity and is produced by an
// IdentityProvider / SecurityIdentityAugmentor, NOT written imperatively from a
// servlet filter. The Spring Security imports have been replaced with the
// stirling.software.common.security compat shims so this file compiles; once the
// collaborators are ported to Quarkus security, this filter should register the user
// via a custom IdentityProvider keyed off the validated JWT claims (prefer
// quarkus-smallrye-jwt for bearer validation) instead of
// UsernamePasswordAuthenticationToken.
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.servlet.Filter;
@@ -45,7 +37,14 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.exception.UnsupportedProviderException;
import stirling.software.proprietary.security.model.ApiKeyAuthenticationToken;
import stirling.software.common.security.Authentication;
import stirling.software.common.security.AuthenticationException;
import stirling.software.common.security.GrantedAuthority;
import stirling.software.common.security.SecurityContextHolder;
import stirling.software.common.security.SimpleGrantedAuthority;
import stirling.software.common.security.UsernameNotFoundException;
import stirling.software.common.security.UsernamePasswordAuthenticationToken;
import stirling.software.proprietary.security.JwtAuthenticationEntryPoint;
import stirling.software.proprietary.security.model.AuthenticationType;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.model.exception.AuthenticationFailureException;
@@ -67,10 +66,10 @@ public class JwtAuthenticationFilter implements Filter {
@Inject JwtServiceInterface jwtService;
@Inject UserService userService;
@Inject CustomUserDetailsService userDetailsService;
// TODO: Migration required - AuthenticationEntryPoint is a Spring Security type.
// JwtAuthenticationEntryPoint is still a Spring @Component; once migrated this should
// be injected as a plain CDI bean (it only writes a 401 JSON/error to the response).
@Inject AuthenticationEntryPoint authenticationEntryPoint;
// JwtAuthenticationEntryPoint is now a plain CDI bean (it only writes a 401
// JSON/error to the response); inject the concrete type instead of the former
// Spring Security AuthenticationEntryPoint interface.
@Inject JwtAuthenticationEntryPoint authenticationEntryPoint;
@Inject ApplicationProperties.Security securityProperties;
@Override
@@ -171,9 +170,21 @@ public class JwtAuthenticationFilter implements Filter {
return false;
}
// TODO: Migration required - the previous ApiKeyAuthenticationToken
// extended Spring Security's AbstractAuthenticationToken. It is now a
// plain POJO that does not implement the security-compat Authentication
// contract, so it cannot be stored in the SecurityContext. Build a
// compat UsernamePasswordAuthenticationToken from the user's authorities
// to keep the API-key authentication intent; in Quarkus this should be a
// SecurityIdentity produced by a custom IdentityProvider for the API key.
List<GrantedAuthority> authorities =
user.get().getAuthorities().stream()
.map(a -> (GrantedAuthority) new SimpleGrantedAuthority(
a.getAuthority()))
.toList();
authentication =
new ApiKeyAuthenticationToken(
user.get(), apiKey, user.get().getAuthorities());
new UsernamePasswordAuthenticationToken(
user.get(), apiKey, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
return true;
} catch (AuthenticationException e) {
@@ -202,14 +213,24 @@ public class JwtAuthenticationFilter implements Filter {
// (userDetailsService.loadUserByUsername) can be kept as a plain service call.
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
processUserAuthenticationType(claims, username);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// loadUserByUsername now returns the User entity directly (the former
// UserDetailsService/UserDetails Spring contract was dropped during migration).
User userDetails = userDetailsService.loadUserByUsername(username);
if (userDetails != null) {
List<GrantedAuthority> authorities =
userDetails.getAuthorities().stream()
.map(a -> (GrantedAuthority) new SimpleGrantedAuthority(
a.getAuthority()))
.toList();
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// TODO: Migration required - Spring's WebAuthenticationDetailsSource
// (remote address + session id) has no Quarkus equivalent. Storing the
// request as the details object keeps the call compile-safe; in Quarkus
// this metadata is available from the RoutingContext / SecurityIdentity.
authToken.setDetails(request);
SecurityContextHolder.getContext().setAuthentication(authToken);
} else {
throw new UsernameNotFoundException("User not found: " + username);
@@ -244,10 +265,11 @@ public class JwtAuthenticationFilter implements Filter {
}
}
// Accepts any Exception so both the application's AuthenticationFailureException
// (extends RuntimeException) and the security-compat AuthenticationException can be
// passed through to the entry point, which shapes the 401 response.
private void handleAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
HttpServletRequest request, HttpServletResponse response, Exception authException)
throws IOException, ServletException {
authenticationEntryPoint.commence(request, response, authException);
}
@@ -6,20 +6,14 @@ import java.io.IOException;
import java.util.List;
import java.util.Optional;
// TODO: Migration required - the following Spring Security core types are still on the
// collaborator APIs (UserService, SessionPersistentRegistry, ApiKeyAuthenticationToken) which are
// NOT yet migrated to Quarkus. Once those collaborators move to io.quarkus.security.identity
// (SecurityIdentity) + a SecurityIdentityAugmentor, replace SecurityContextHolder/Authentication
// with an injected SecurityIdentity (or @Context jakarta.ws.rs.core.SecurityContext) and drop these
// imports. The principal-type dispatch (UserDetails/OAuth2User/CustomSaml2AuthenticatedPrincipal)
// must then be re-expressed via SecurityIdentity attributes/roles.
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
// TODO: Migration required - authentication state currently flows through the
// stirling.software.common.security compat shims (SecurityContextHolder/Authentication) backed by
// the collaborator APIs (UserService, SessionPersistentRegistry, ApiKeyAuthenticationToken). Once
// those collaborators move to io.quarkus.security.identity (SecurityIdentity) + a
// SecurityIdentityAugmentor, replace SecurityContextHolder/Authentication with an injected
// SecurityIdentity (or @Context jakarta.ws.rs.core.SecurityContext). The principal-type dispatch
// (UserDetails/OAuth2User/CustomSaml2AuthenticatedPrincipal) must then be re-expressed via
// SecurityIdentity attributes/roles.
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
@@ -37,6 +31,12 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.common.model.ApplicationProperties.Security.SAML2;
import stirling.software.common.security.Authentication;
import stirling.software.common.security.AuthenticationException;
import stirling.software.common.security.OAuth2User;
import stirling.software.common.security.SecurityContextHolder;
import stirling.software.common.security.SessionInformation;
import stirling.software.common.security.UserDetails;
import stirling.software.common.util.RequestUriUtils;
import stirling.software.proprietary.security.model.ApiKeyAuthenticationToken;
import stirling.software.proprietary.security.model.User;
@@ -1,19 +1,11 @@
package stirling.software.proprietary.security.service;
// TODO: Migration required - org.springframework.mail.javamail.* is Spring's mail abstraction (NOT
// Spring DI) and Quarkus has no drop-in equivalent. quarkus-mailer (io.quarkus.mailer.Mailer /
// ReactiveMailer) exposes a different API and would require migrating the collaborator MailConfig
// (which still produces a JavaMailSender) together with this service. The JavaMailSender /
// MimeMessage / MimeMessageHelper logic is kept unchanged until that joint migration; only the DI
// glue has been converted. When migrating, swap to quarkus.mailer.* config + io.quarkus.mailer.Mail.
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import io.quarkus.mailer.Mail;
import io.quarkus.mailer.Mailer;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.util.ByteArrayDataSource;
import lombok.extern.slf4j.Slf4j;
@@ -22,32 +14,31 @@ import stirling.software.common.model.MultipartFile;
import stirling.software.proprietary.security.model.api.Email;
/**
* Service class responsible for sending emails, including those with attachments. It uses
* JavaMailSender to send the email and is designed to handle both the message content and file
* attachments.
* Service class responsible for sending emails, including those with attachments. It uses the
* Quarkus {@link Mailer} to send the email and is designed to handle both the message content and
* file attachments.
*/
// TODO: Migration required - the original class was guarded by
// @ConditionalOnProperty(value = "mail.enabled", havingValue = "true", matchIfMissing = false).
// Quarkus has no @ConditionalOnProperty. mail.enabled is a runtime property
// (ApplicationProperties.Mail#isEnabled) rather than a build-time flag, so the bean is always
// produced and callers must guard on applicationProperties.getMail().isEnabled() at call time
// (matching the decision already made in the MailConfig producer).
// produced and callers must guard on applicationProperties.getMail().isEnabled() at call time.
// SMTP connection settings now live under quarkus.mailer.* config instead of MailConfig.
@Slf4j
@ApplicationScoped
public class EmailService {
private final JavaMailSender mailSender;
private final Mailer mailer;
private final ApplicationProperties applicationProperties;
@Inject
public EmailService(JavaMailSender mailSender, ApplicationProperties applicationProperties) {
this.mailSender = mailSender;
public EmailService(Mailer mailer, ApplicationProperties applicationProperties) {
this.mailer = mailer;
this.applicationProperties = applicationProperties;
}
/**
* Sends an email with an attachment asynchronously. This method is annotated with @Async, which
* means it will be executed asynchronously.
* Sends an email with an attachment.
*
* @param email The Email object containing the recipient, subject, body, and file attachment.
* @throws MessagingException If there is an issue with creating or sending the email.
@@ -73,35 +64,24 @@ public class EmailService {
ApplicationProperties.Mail mailProperties = applicationProperties.getMail();
// Creates a MimeMessage to represent the email
MimeMessage message = mailSender.createMimeMessage();
// Build an HTML mail (the "true" body content is HTML).
Mail mail =
Mail.withHtml(email.getTo(), email.getSubject(), email.getBody())
.setFrom(mailProperties.getFrom());
// Helper class to set up the message content and attachments
MimeMessageHelper helper = new MimeMessageHelper(message, true);
// Sets the recipient, subject, body, and sender email
helper.addTo(email.getTo());
helper.setSubject(email.getSubject());
helper.setText(
email.getBody(),
true); // The "true" here indicates that the body contains HTML content.
helper.setFrom(mailProperties.getFrom());
// Adds the attachment to the email. The common MultipartFile shim is not a Spring
// InputStreamSource, so wrap its bytes in a jakarta.mail DataSource (pure Jakarta Mail API).
// Adds the attachment to the email using the Quarkus Mail attachment API.
try {
String contentType = file.getContentType();
ByteArrayDataSource dataSource =
new ByteArrayDataSource(
file.getBytes(),
contentType != null ? contentType : "application/octet-stream");
helper.addAttachment(file.getOriginalFilename(), dataSource);
mail.addAttachment(
file.getOriginalFilename(),
file.getBytes(),
contentType != null ? contentType : "application/octet-stream");
} catch (java.io.IOException e) {
throw new MessagingException("Failed to read attachment content", e);
}
// Sends the email via the configured mail sender
mailSender.send(message);
// Sends the email via the configured mailer
mailer.send(mail);
log.debug(
"Email sent successfully to {} with subject: {} body: {}",
email.getTo(),
@@ -110,7 +90,7 @@ public class EmailService {
}
/**
* Sends a simple email without attachments asynchronously.
* Sends a simple plain-text email without attachments.
*
* @param to the recipient address
* @param subject subject line
@@ -124,13 +104,8 @@ public class EmailService {
}
ApplicationProperties.Mail mailProperties = applicationProperties.getMail();
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, false);
helper.addTo(to);
helper.setSubject(subject);
helper.setText(body, false);
helper.setFrom(mailProperties.getFrom());
mailSender.send(message);
Mail mail = Mail.withText(to, subject, body).setFrom(mailProperties.getFrom());
mailer.send(mail);
log.debug(
"Simple email sent successfully to {} with subject: {} body: {}",
to,
@@ -139,7 +114,7 @@ public class EmailService {
}
/**
* Sends a plain text/HTML email without attachments asynchronously.
* Sends a plain text/HTML email without attachments.
*
* @param to The recipient email address
* @param subject The email subject
@@ -157,20 +132,14 @@ public class EmailService {
ApplicationProperties.Mail mailProperties = applicationProperties.getMail();
// Creates a MimeMessage to represent the email
MimeMessage message = mailSender.createMimeMessage();
Mail mail =
isHtml
? Mail.withHtml(to, subject, body)
: Mail.withText(to, subject, body);
mail.setFrom(mailProperties.getFrom());
// Helper class to set up the message content
MimeMessageHelper helper = new MimeMessageHelper(message, false);
// Sets the recipient, subject, body, and sender email
helper.addTo(to);
helper.setSubject(subject);
helper.setText(body, isHtml);
helper.setFrom(mailProperties.getFrom());
// Sends the email via the configured mail sender
mailSender.send(message);
// Sends the email via the configured mailer
mailer.send(mail);
}
/**
@@ -13,14 +13,9 @@ import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
// TODO: Migration required - JwtServiceInterface still declares generateToken(Authentication, ...)
// using org.springframework.security.core.Authentication. The interface (a separate file) must be
// migrated too; once it switches to io.quarkus.security.identity.SecurityIdentity, update the
// implementation below and drop these Spring Security imports. Kept here to satisfy the interface
// contract without changing a collaborator file.
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import stirling.software.common.security.Authentication;
import stirling.software.common.security.OAuth2User;
import stirling.software.common.security.UserDetails;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
@@ -21,31 +21,19 @@ import jakarta.transaction.Transactional;
import org.slf4j.MDC;
// TODO: Migration required - Spring Security glue retained until the security layer is migrated.
// SecurityContextHolder/Authentication should become io.quarkus.security.identity.SecurityIdentity
// (injected) or @Context jakarta.ws.rs.core.SecurityContext; UsernamePasswordAuthenticationToken /
// GrantedAuthority / UserDetails / UsernameNotFoundException / OAuth2User / SessionInformation are
// produced and consumed by collaborators not yet ported (SessionPersistentRegistry, the auth
// filters, CustomUserDetailsService). These types are kept so the public bridge methods
// (getAuthentication, getCurrentUsername, invalidateUserSessions, isCurrentUserAdmin) keep working
// until those collaborators are converted together.
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.core.user.OAuth2User;
// TODO: Migration required - spring-security-crypto is the agreed temporary shim. PasswordEncoder is
// produced by PasswordEncoderConfig (see that file's class-level note); replace this import once a
// Quarkus-compatible BCrypt abstraction is wired across UserService + PasswordEncoderConfig +
// SecurityConfiguration together.
import org.springframework.security.crypto.password.PasswordEncoder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.security.Authentication;
import stirling.software.common.security.GrantedAuthority;
import stirling.software.common.security.OAuth2User;
import stirling.software.common.security.PasswordEncoder;
import stirling.software.common.security.SecurityContextHolder;
import stirling.software.common.security.SimpleGrantedAuthority;
import stirling.software.common.security.SessionInformation;
import stirling.software.common.security.UserDetails;
import stirling.software.common.security.UsernamePasswordAuthenticationToken;
import stirling.software.common.security.UsernameNotFoundException;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.enumeration.Role;
import stirling.software.common.model.exception.UnsupportedProviderException;
@@ -170,7 +158,11 @@ public class UserService implements UserServiceInterface {
}
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
return user.getAuthorities();
// User.getAuthorities() returns Set<Authority> (a JPA entity) which does not implement the
// GrantedAuthority shim; adapt each Authority's role string into a SimpleGrantedAuthority.
return user.getAuthorities().stream()
.map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
.toList();
}
private String generateApiKey() {
@@ -672,7 +664,11 @@ public class UserService implements UserServiceInterface {
} else if (principal instanceof User domainUser) {
return domainUser.getUsername();
} else if (principal instanceof OAuth2User oAuth2User) {
return oAuth2User.getAttribute(oAuth2.getUseAsUsername());
// OAuth2User shim exposes getAttributes() (Map) but not the singular
// getAttribute(String) convenience accessor; read from the map directly.
Object usernameAttr =
oAuth2User.getAttributes().get(oAuth2.getUseAsUsername());
return usernameAttr != null ? usernameAttr.toString() : null;
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
return saml2User.name();
} else if (principal instanceof String stringUser) {
@@ -8,10 +8,10 @@ import java.util.List;
import java.util.Optional;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import stirling.software.common.security.OAuth2User;
import stirling.software.common.security.SessionInformation;
import stirling.software.common.security.SessionRegistry;
import stirling.software.common.security.UserDetails;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;
@@ -22,14 +22,14 @@ import stirling.software.proprietary.security.database.repository.SessionReposit
import stirling.software.proprietary.security.model.SessionEntity;
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
// TODO: Migration required - this class implements Spring Security's SessionRegistry
// (org.springframework.security.core.session.SessionRegistry) and exposes SessionInformation,
// UserDetails and OAuth2User from spring-security. Quarkus has no equivalent session-registry
// abstraction. The Spring Security imports below are kept ONLY because un-migrated collaborators
// TODO: Migration required - this class implements the SessionRegistry compatibility shim
// (stirling.software.common.security.SessionRegistry) and exposes SessionInformation,
// UserDetails and OAuth2User from the same compat package. Quarkus has no equivalent
// session-registry abstraction. These shim types are kept ONLY because un-migrated collaborators
// (UserAuthenticationFilter, UserService, SessionRegistryConfig) still consume this interface and
// its return types. Once those collaborators are migrated to Quarkus security
// (io.quarkus.security.identity.SecurityIdentity), this class should drop the SessionRegistry
// contract and the spring-security types, replacing them with a plain CDI service over the
// contract and the compat security types, replacing them with a plain CDI service over the
// SessionEntity table.
@ApplicationScoped
@RequiredArgsConstructor
@@ -5,11 +5,7 @@ import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.List;
// TODO: Migration required - SessionInformation is a Spring Security type
// (org.springframework.security.core.session.SessionInformation) still returned by the
// not-yet-migrated collaborator SessionPersistentRegistry. Keep this import until that
// collaborator and the session-registry abstraction are migrated off Spring Security.
import org.springframework.security.core.session.SessionInformation;
import stirling.software.common.security.SessionInformation;
import io.quarkus.scheduler.Scheduled;
@@ -2,12 +2,8 @@ package stirling.software.proprietary.service;
import java.io.IOException;
// TODO: Migration required - AiEngineClient (a collaborator, not yet migrated) still throws
// org.springframework.web.server.ResponseStatusException, so this import must stay until that file
// is converted. Once AiEngineClient throws jakarta.ws.rs.WebApplicationException, swap this catch.
import org.springframework.web.server.ResponseStatusException;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.WebApplicationException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -48,8 +44,8 @@ public class AiUserDataService {
try {
aiEngineClient.delete(PURGE_PATH, userId);
log.debug("Requested document purge for user {}", userId);
} catch (ResponseStatusException e) {
log.warn("AI engine refused document purge for {}: {}", userId, e.getReason());
} catch (WebApplicationException e) {
log.warn("AI engine refused document purge for {}: {}", userId, e.getMessage());
} catch (IOException e) {
log.warn("Failed to purge documents for {}: {}", userId, e.getMessage());
}
@@ -21,17 +21,13 @@ import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.MDC;
// TODO: Migration required - org.springframework.boot.actuate.audit.AuditEvent and
// AuditEventRepository are Spring Boot Actuator types with no Quarkus equivalent. They are kept
// here because this is a coordinated migration: stirling.software.proprietary.config
// .CustomAuditEventRepository implements AuditEventRepository and several controllers/services
// (AuditRestController, AuditDashboardController, AuditCleanupService, PersistentAuditEventRepository)
// share these types. Replace them across that set with a plain audit-record DTO + a CDI-managed
// repository (PanacheRepository over the existing PersistentAuditEvent entity) when that set is
// migrated; the persistence logic below (repository.add(new AuditEvent(...))) is preserved verbatim.
import org.springframework.boot.actuate.audit.AuditEvent;
import org.springframework.boot.actuate.audit.AuditEventRepository;
// Migration: org.springframework.boot.actuate.audit.AuditEvent and AuditEventRepository were Spring
// Boot Actuator types with no Quarkus equivalent. The write side has already been ported to a plain
// CDI bean (stirling.software.proprietary.config.CustomAuditEventRepository) whose add(principal,
// type, timestamp, data) method persists a PersistentAuditEvent. This service now depends on that
// bean directly; the old `repository.add(new AuditEvent(principal, type, data))` calls have been
// rewritten to `repository.add(principal, type, Instant.now(), data)`, preserving persistence
// behavior without the Actuator DTO.
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.enterprise.context.ApplicationScoped;
@@ -53,6 +49,7 @@ import stirling.software.proprietary.audit.AuditEventType;
import stirling.software.proprietary.audit.AuditLevel;
import stirling.software.proprietary.audit.Audited;
import stirling.software.proprietary.config.AuditConfigurationProperties;
import stirling.software.proprietary.config.CustomAuditEventRepository;
import stirling.software.proprietary.security.service.JwtServiceInterface;
/**
@@ -63,7 +60,7 @@ import stirling.software.proprietary.security.service.JwtServiceInterface;
@ApplicationScoped
public class AuditService {
private final AuditEventRepository repository;
private final CustomAuditEventRepository repository;
private final AuditConfigurationProperties auditConfig;
private final boolean runningEE;
private final CustomPDFDocumentFactory pdfDocumentFactory;
@@ -81,7 +78,7 @@ public class AuditService {
@Inject
public AuditService(
AuditEventRepository repository,
CustomAuditEventRepository repository,
AuditConfigurationProperties auditConfig,
@Named("runningEE") boolean runningEE,
CustomPDFDocumentFactory pdfDocumentFactory,
@@ -121,7 +118,7 @@ public class AuditService {
Map<String, Object> enrichedData = new java.util.HashMap<>(data);
enrichedData.put("__origin", determineOrigin());
repository.add(new AuditEvent(principal, type.name(), enrichedData));
repository.add(principal, type.name(), Instant.now(), enrichedData);
}
/**
@@ -152,7 +149,7 @@ public class AuditService {
return;
}
repository.add(new AuditEvent(principal, type.name(), data));
repository.add(principal, type.name(), Instant.now(), data);
}
/**
@@ -188,7 +185,7 @@ public class AuditService {
Map<String, Object> enrichedData = new java.util.HashMap<>(data);
enrichedData.put("__origin", determineOrigin());
repository.add(new AuditEvent(principal, type, enrichedData));
repository.add(principal, type, Instant.now(), enrichedData);
}
/**
@@ -218,7 +215,7 @@ public class AuditService {
return;
}
repository.add(new AuditEvent(principal, type, data));
repository.add(principal, type, Instant.now(), data);
}
/**
@@ -257,7 +254,7 @@ public class AuditService {
enrichedData.put("__ipAddress", ipAddress);
}
repository.add(new AuditEvent(principal, type.name(), enrichedData));
repository.add(principal, type.name(), Instant.now(), enrichedData);
}
/**
@@ -284,7 +281,7 @@ public class AuditService {
enrichedData.put("__ipAddress", ipAddress);
}
repository.add(new AuditEvent(principal, type, enrichedData));
repository.add(principal, type, Instant.now(), enrichedData);
}
// ========== DATA COLLECTION METHODS ==========
@@ -23,8 +23,8 @@ import java.util.stream.Collectors;
// io.quarkus.security.identity.SecurityIdentity (injected) or @Context
// jakarta.ws.rs.core.SecurityContext once the storage controllers are migrated; the principal is
// expected to be a stirling.software.proprietary.security.model.User instance.
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import stirling.software.common.security.Authentication;
import stirling.software.common.security.SecurityContextHolder;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.mail.MessagingException;
@@ -7,12 +7,6 @@ import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
// TODO: Migration required - org.springframework.web.server.ResponseStatusException is still
// thrown by the not-yet-migrated CertificateSubmissionValidator service (validateAndExtractInfo).
// Keep catching it here until that collaborator is converted to throw WebApplicationException;
// then this import and the catch blocks below can be replaced.
import org.springframework.web.server.ResponseStatusException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -219,14 +213,9 @@ public class WorkflowParticipantController {
return Response.ok(WorkflowMapper.toParticipantResponse(participant, false)).build();
} catch (WebApplicationException e) {
// CertificateSubmissionValidator now throws WebApplicationException on validation
// failure (post Spring->Quarkus migration); propagate as-is.
throw e;
} catch (ResponseStatusException e) {
// Thrown by the not-yet-migrated CertificateSubmissionValidator on validation failure.
// TODO: Migration required - replace with WebApplicationException once that service is
// converted.
throw new WebApplicationException(
e.getReason(),
Response.Status.fromStatusCode(e.getStatusCode().value()));
} catch (Exception e) {
log.error("Error submitting signature for participant {}", participant.getEmail(), e);
throw new WebApplicationException(
@@ -399,13 +388,13 @@ public class WorkflowParticipantController {
null))
.build();
} catch (ResponseStatusException e) {
} catch (WebApplicationException e) {
// Validation failure return 200 with valid:false so the frontend can display inline.
// TODO: Migration required - CertificateSubmissionValidator still throws Spring's
// ResponseStatusException; switch to WebApplicationException once it is converted.
// CertificateSubmissionValidator throws WebApplicationException (post migration); use
// its message as the inline error reason.
return Response.ok(
new CertificateValidationResponse(
false, null, null, null, null, false, e.getReason()))
false, null, null, null, null, false, e.getMessage()))
.build();
} catch (IOException e) {
log.error("Error reading certificate file during pre-validation", e);