diff --git a/app/common/src/main/java/stirling/software/common/configuration/ApplicationPropertiesConfigOverlay.java b/app/common/src/main/java/stirling/software/common/configuration/ApplicationPropertiesConfigOverlay.java new file mode 100644 index 0000000000..bb6f08a016 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/configuration/ApplicationPropertiesConfigOverlay.java @@ -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. + * + *

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). + * + *

Runs before {@link stirling.software.proprietary.security.InitialSecuritySetup} (low + * {@code @Priority}) because that startup step reads {@code customGlobalAPIKey} and {@code + * enableLogin}. + * + *

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 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 setter) { + config.getOptionalValue(key, String.class) + .ifPresent( + value -> { + setter.accept(value); + log.info("Applied config override {}={}", key, value); + }); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/identity/UserSecurityIdentityAugmentor.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/identity/UserSecurityIdentityAugmentor.java new file mode 100644 index 0000000000..77c0179b0c --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/identity/UserSecurityIdentityAugmentor.java @@ -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 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 userOpt = userService.findByUsernameIgnoreCase(username); + if (userOpt.isEmpty()) { + return identity; + } + return QuarkusSecurityIdentity.builder(identity).setPrincipal(userOpt.get()).build(); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/model/User.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/model/User.java index a1bcecefa5..b30753dfff 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/model/User.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/model/User.java @@ -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") diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java index fe14ef9c99..8ce6a540b3 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java @@ -372,6 +372,7 @@ public class UserService implements UserServiceInterface { return user; } + @Transactional public Optional findByUsernameIgnoreCase(String username) { return userRepository.findByUsernameIgnoreCase(username); }