feat: wire AuditService, enable method security, retrofit audit logging into existing controllers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,11 +3,15 @@ package com.cameleer3.server.app.security;
|
||||
import com.cameleer3.server.app.dto.AuthTokenResponse;
|
||||
import com.cameleer3.server.app.dto.ErrorResponse;
|
||||
import com.cameleer3.server.app.dto.OidcPublicConfigResponse;
|
||||
import com.cameleer3.server.core.admin.AuditCategory;
|
||||
import com.cameleer3.server.core.admin.AuditResult;
|
||||
import com.cameleer3.server.core.admin.AuditService;
|
||||
import com.cameleer3.server.core.security.JwtService;
|
||||
import com.cameleer3.server.core.security.OidcConfig;
|
||||
import com.cameleer3.server.core.security.OidcConfigRepository;
|
||||
import com.cameleer3.server.core.security.UserInfo;
|
||||
import com.cameleer3.server.core.security.UserRepository;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@@ -27,6 +31,7 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
import java.net.URI;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
@@ -46,15 +51,18 @@ public class OidcAuthController {
|
||||
private final OidcConfigRepository configRepository;
|
||||
private final JwtService jwtService;
|
||||
private final UserRepository userRepository;
|
||||
private final AuditService auditService;
|
||||
|
||||
public OidcAuthController(OidcTokenExchanger tokenExchanger,
|
||||
OidcConfigRepository configRepository,
|
||||
JwtService jwtService,
|
||||
UserRepository userRepository) {
|
||||
UserRepository userRepository,
|
||||
AuditService auditService) {
|
||||
this.tokenExchanger = tokenExchanger;
|
||||
this.configRepository = configRepository;
|
||||
this.jwtService = jwtService;
|
||||
this.userRepository = userRepository;
|
||||
this.auditService = auditService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,7 +108,8 @@ public class OidcAuthController {
|
||||
@ApiResponse(responseCode = "403", description = "Account not provisioned",
|
||||
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@ApiResponse(responseCode = "404", description = "OIDC not configured or disabled")
|
||||
public ResponseEntity<AuthTokenResponse> callback(@RequestBody CallbackRequest request) {
|
||||
public ResponseEntity<AuthTokenResponse> callback(@RequestBody CallbackRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
Optional<OidcConfig> config = configRepository.find();
|
||||
if (config.isEmpty() || !config.get().enabled()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
@@ -132,6 +141,8 @@ public class OidcAuthController {
|
||||
|
||||
String displayName = oidcUser.name() != null && !oidcUser.name().isBlank()
|
||||
? oidcUser.name() : oidcUser.email();
|
||||
auditService.log(userId, "login_oidc", AuditCategory.AUTH, null,
|
||||
Map.of("provider", config.get().issuerUri()), AuditResult.SUCCESS, httpRequest);
|
||||
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken, displayName, oidcUser.idToken()));
|
||||
} catch (ResponseStatusException e) {
|
||||
throw e;
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
@@ -27,6 +28,7 @@ import java.util.List;
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -2,7 +2,11 @@ package com.cameleer3.server.app.security;
|
||||
|
||||
import com.cameleer3.server.app.dto.AuthTokenResponse;
|
||||
import com.cameleer3.server.app.dto.ErrorResponse;
|
||||
import com.cameleer3.server.core.admin.AuditCategory;
|
||||
import com.cameleer3.server.core.admin.AuditResult;
|
||||
import com.cameleer3.server.core.admin.AuditService;
|
||||
import com.cameleer3.server.core.security.JwtService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import com.cameleer3.server.core.security.JwtService.JwtValidationResult;
|
||||
import com.cameleer3.server.core.security.UserInfo;
|
||||
import com.cameleer3.server.core.security.UserRepository;
|
||||
@@ -23,6 +27,7 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Authentication endpoints for the UI (local credentials).
|
||||
@@ -41,12 +46,14 @@ public class UiAuthController {
|
||||
private final JwtService jwtService;
|
||||
private final SecurityProperties properties;
|
||||
private final UserRepository userRepository;
|
||||
private final AuditService auditService;
|
||||
|
||||
public UiAuthController(JwtService jwtService, SecurityProperties properties,
|
||||
UserRepository userRepository) {
|
||||
UserRepository userRepository, AuditService auditService) {
|
||||
this.jwtService = jwtService;
|
||||
this.properties = properties;
|
||||
this.userRepository = userRepository;
|
||||
this.auditService = auditService;
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
@@ -54,19 +61,24 @@ public class UiAuthController {
|
||||
@ApiResponse(responseCode = "200", description = "Login successful")
|
||||
@ApiResponse(responseCode = "401", description = "Invalid credentials",
|
||||
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
public ResponseEntity<AuthTokenResponse> login(@RequestBody LoginRequest request) {
|
||||
public ResponseEntity<AuthTokenResponse> login(@RequestBody LoginRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
String configuredUser = properties.getUiUser();
|
||||
String configuredPassword = properties.getUiPassword();
|
||||
|
||||
if (configuredUser == null || configuredUser.isBlank()
|
||||
|| configuredPassword == null || configuredPassword.isBlank()) {
|
||||
log.warn("UI authentication attempted but CAMELEER_UI_USER / CAMELEER_UI_PASSWORD not configured");
|
||||
auditService.log(request.username(), "login_failed", AuditCategory.AUTH, null,
|
||||
Map.of("reason", "UI authentication not configured"), AuditResult.FAILURE, httpRequest);
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "UI authentication not configured");
|
||||
}
|
||||
|
||||
if (!configuredUser.equals(request.username())
|
||||
|| !configuredPassword.equals(request.password())) {
|
||||
log.debug("UI login failed for user: {}", request.username());
|
||||
auditService.log(request.username(), "login_failed", AuditCategory.AUTH, null,
|
||||
Map.of("reason", "Invalid credentials"), AuditResult.FAILURE, httpRequest);
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials");
|
||||
}
|
||||
|
||||
@@ -84,6 +96,7 @@ public class UiAuthController {
|
||||
String accessToken = jwtService.createAccessToken(subject, "user", roles);
|
||||
String refreshToken = jwtService.createRefreshToken(subject, "user", roles);
|
||||
|
||||
auditService.log(request.username(), "login", AuditCategory.AUTH, null, null, AuditResult.SUCCESS, httpRequest);
|
||||
log.info("UI user logged in: {}", request.username());
|
||||
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken, request.username(), null));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user