Add Spring Security compat shim; clear residual proprietary Spring imports
This commit is contained in:
@@ -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'
|
||||
|
||||
+70
@@ -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();
|
||||
}
|
||||
+19
@@ -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;
|
||||
}
|
||||
}
|
||||
+18
@@ -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);
|
||||
}
|
||||
+41
@@ -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;
|
||||
}
|
||||
}
|
||||
+20
@@ -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);
|
||||
}
|
||||
+45
@@ -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;
|
||||
}
|
||||
+18
@@ -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);
|
||||
}
|
||||
}
|
||||
+75
@@ -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();
|
||||
}
|
||||
}
|
||||
+82
-91
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+38
-35
@@ -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(
|
||||
|
||||
+11
-12
@@ -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 "
|
||||
|
||||
+32
-32
@@ -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();
|
||||
|
||||
+1
-4
@@ -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;
|
||||
|
||||
+26
-97
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+6
-9
@@ -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 {
|
||||
|
||||
+3
-8
@@ -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;
|
||||
|
||||
|
||||
+7
-7
@@ -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;
|
||||
|
||||
+2
-11
@@ -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;
|
||||
|
||||
|
||||
+55
-33
@@ -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);
|
||||
}
|
||||
|
||||
+14
-14
@@ -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;
|
||||
|
||||
+33
-64
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+3
-8
@@ -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;
|
||||
|
||||
+20
-24
@@ -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) {
|
||||
|
||||
+9
-9
@@ -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
|
||||
|
||||
+1
-5
@@ -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;
|
||||
|
||||
|
||||
+3
-7
@@ -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());
|
||||
}
|
||||
|
||||
+16
-19
@@ -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 ==========
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+6
-17
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user