Bind security/storage config from env and attach User as SecurityIdentity principal
This commit is contained in:
+82
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
+50
@@ -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();
|
||||
}
|
||||
}
|
||||
+11
-1
@@ -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")
|
||||
|
||||
+1
@@ -372,6 +372,7 @@ public class UserService implements UserServiceInterface {
|
||||
return user;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Optional<User> findByUsernameIgnoreCase(String username) {
|
||||
return userRepository.findByUsernameIgnoreCase(username);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user