Bind security/storage config from env and attach User as SecurityIdentity principal

This commit is contained in:
a
2026-06-13 10:24:12 +01:00
parent 5679967b1b
commit 4008161b93
4 changed files with 144 additions and 1 deletions
@@ -0,0 +1,82 @@
package stirling.software.common.configuration;
import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
import io.quarkus.runtime.StartupEvent;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import jakarta.interceptor.Interceptor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
/**
* Overlays MicroProfile/Quarkus config (env vars, application.properties) onto {@link
* ApplicationProperties} at startup.
*
* <p>The Spring {@code @ConfigurationProperties} binding that populated {@code
* ApplicationProperties} from {@code settings.yml} + env was never migrated, so the POJO otherwise
* carries only its Java defaults (this is the root cause behind the {@code maxDPI=0} / {@code
* loginAttemptCount=0} class of bugs and ignored {@code SECURITY_*}/{@code STORAGE_*} env vars).
* SmallRye maps an env var like {@code SECURITY_ENABLELOGIN} to the property {@code
* security.enableLogin}, so each key below is pulled from config and applied in place on the shared
* bean (nested objects are referenced by the {@code AppConfig} producers, so mutating them
* propagates everywhere).
*
* <p>Runs before {@link stirling.software.proprietary.security.InitialSecuritySetup} (low
* {@code @Priority}) because that startup step reads {@code customGlobalAPIKey} and {@code
* enableLogin}.
*
* <p>TODO: this is a focused subset (auth/storage/SSO). A complete migration would bind every
* ApplicationProperties field generically (e.g. via {@code @ConfigMapping} or a reflective overlay)
* and also read {@code settings.yml}.
*/
@Slf4j
@ApplicationScoped
public class ApplicationPropertiesConfigOverlay {
@Inject ApplicationProperties applicationProperties;
void onStart(@Observes @Priority(Interceptor.Priority.APPLICATION) StartupEvent event) {
Config config = ConfigProvider.getConfig();
ApplicationProperties.Security security = applicationProperties.getSecurity();
applyBoolean(config, "security.enableLogin", security::setEnableLogin);
applyString(config, "security.loginMethod", security::setLoginMethod);
applyString(config, "security.customGlobalAPIKey", security::setCustomGlobalAPIKey);
applyBoolean(config, "storage.enabled", applicationProperties.getStorage()::setEnabled);
// SSO toggles - the detailed provider config is bound by the OIDC/SAML wiring.
if (security.getSaml2() != null) {
applyBoolean(config, "security.saml2.enabled", security.getSaml2()::setEnabled);
}
if (security.getOauth2() != null) {
applyBoolean(config, "security.oauth2.enabled", security.getOauth2()::setEnabled);
}
}
private void applyBoolean(
Config config, String key, java.util.function.Consumer<Boolean> setter) {
config.getOptionalValue(key, Boolean.class)
.ifPresent(
value -> {
setter.accept(value);
log.info("Applied config override {}={}", key, value);
});
}
private void applyString(
Config config, String key, java.util.function.Consumer<String> setter) {
config.getOptionalValue(key, String.class)
.ifPresent(
value -> {
setter.accept(value);
log.info("Applied config override {}={}", key, value);
});
}
}
@@ -0,0 +1,50 @@
package stirling.software.proprietary.security.identity;
import java.util.Optional;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.service.UserService;
/**
* Attaches the {@link User} entity as the {@link SecurityIdentity} principal for any authenticated
* request. Spring exposed the {@code User} directly via {@code Authentication#getPrincipal()} (it
* implemented {@code UserDetails}), so a lot of the code base does {@code principal instanceof
* User} (folders, file storage, sessions, audit, UserController). This augmentor restores that for
* the Quarkus auth paths (JWT Bearer, X-API-KEY, and later OIDC/SAML): it re-loads the user by name
* and rebuilds the identity with the {@code User} as principal, preserving the roles the {@link
* JwtTokenIdentityProvider} / {@link ApiKeyIdentityProvider} already assigned. This is the
* augmentor the {@code // TODO: Migration required} comments across the security/storage code ask
* for.
*/
@ApplicationScoped
public class UserSecurityIdentityAugmentor implements SecurityIdentityAugmentor {
@Inject UserService userService;
@Override
public Uni<SecurityIdentity> augment(
SecurityIdentity identity, AuthenticationRequestContext context) {
if (identity == null || identity.isAnonymous() || identity.getPrincipal() instanceof User) {
return Uni.createFrom().item(identity);
}
return context.runBlocking(() -> withUserPrincipal(identity));
}
private SecurityIdentity withUserPrincipal(SecurityIdentity identity) {
String username = identity.getPrincipal().getName();
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (userOpt.isEmpty()) {
return identity;
}
return QuarkusSecurityIdentity.builder(identity).setPrincipal(userOpt.get()).build();
}
}
@@ -1,6 +1,7 @@
package stirling.software.proprietary.security.model;
import java.io.Serializable;
import java.security.Principal;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.HashSet;
@@ -40,10 +41,19 @@ import stirling.software.proprietary.model.Team;
// entity. The Lombok getters still expose getUsername()/getPassword()/getAuthorities()/
// isEnabled() so that adapter can read them directly. isEnabled() override below is retained
// as plain business logic (null-safe enabled flag).
public class User implements Serializable {
public class User implements Serializable, Principal {
private static final long serialVersionUID = 1L;
// Principal#getName - lets the User entity itself be the Quarkus SecurityIdentity principal, so
// the many `principal instanceof User` call sites (folders, file storage, sessions, audit) keep
// working. @JsonIgnore so it does not add a "name" field to serialized User JSON.
@Override
@JsonIgnore
public String getName() {
return username;
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
@@ -372,6 +372,7 @@ public class UserService implements UserServiceInterface {
return user;
}
@Transactional
public Optional<User> findByUsernameIgnoreCase(String username) {
return userRepository.findByUsernameIgnoreCase(username);
}