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:
hsiegeln
2026-03-17 15:51:22 +01:00
parent e8842e3bdc
commit 1d6ae00b1c
6 changed files with 73 additions and 11 deletions

View File

@@ -5,8 +5,12 @@ import com.cameleer3.server.app.dto.OidcAdminConfigRequest;
import com.cameleer3.server.app.dto.OidcAdminConfigResponse;
import com.cameleer3.server.app.dto.OidcTestResult;
import com.cameleer3.server.app.security.OidcTokenExchanger;
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.OidcConfig;
import com.cameleer3.server.core.security.OidcConfigRepository;
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;
@@ -16,6 +20,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@@ -26,6 +31,7 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
@@ -35,17 +41,21 @@ import java.util.Optional;
@RestController
@RequestMapping("/api/v1/admin/oidc")
@Tag(name = "OIDC Config Admin", description = "OIDC provider configuration (ADMIN only)")
@PreAuthorize("hasRole('ADMIN')")
public class OidcConfigAdminController {
private static final Logger log = LoggerFactory.getLogger(OidcConfigAdminController.class);
private final OidcConfigRepository configRepository;
private final OidcTokenExchanger tokenExchanger;
private final AuditService auditService;
public OidcConfigAdminController(OidcConfigRepository configRepository,
OidcTokenExchanger tokenExchanger) {
OidcTokenExchanger tokenExchanger,
AuditService auditService) {
this.configRepository = configRepository;
this.tokenExchanger = tokenExchanger;
this.auditService = auditService;
}
@GetMapping
@@ -64,7 +74,8 @@ public class OidcConfigAdminController {
@ApiResponse(responseCode = "200", description = "Configuration saved")
@ApiResponse(responseCode = "400", description = "Invalid configuration",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
public ResponseEntity<OidcAdminConfigResponse> saveConfig(@RequestBody OidcAdminConfigRequest request) {
public ResponseEntity<OidcAdminConfigResponse> saveConfig(@RequestBody OidcAdminConfigRequest request,
HttpServletRequest httpRequest) {
// Resolve client_secret: if masked or empty, preserve existing
String clientSecret = request.clientSecret();
if (clientSecret == null || clientSecret.isBlank() || clientSecret.equals("********")) {
@@ -95,6 +106,7 @@ public class OidcConfigAdminController {
configRepository.save(config);
tokenExchanger.invalidateCache();
auditService.log("update_oidc", AuditCategory.CONFIG, "oidc", Map.of(), AuditResult.SUCCESS, httpRequest);
log.info("OIDC configuration updated: enabled={}, issuer={}", config.enabled(), config.issuerUri());
return ResponseEntity.ok(OidcAdminConfigResponse.from(config));
}
@@ -104,7 +116,7 @@ public class OidcConfigAdminController {
@ApiResponse(responseCode = "200", description = "Provider reachable")
@ApiResponse(responseCode = "400", description = "Provider unreachable or misconfigured",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
public ResponseEntity<OidcTestResult> testConnection() {
public ResponseEntity<OidcTestResult> testConnection(HttpServletRequest httpRequest) {
Optional<OidcConfig> config = configRepository.find();
if (config.isEmpty() || !config.get().enabled()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
@@ -114,6 +126,7 @@ public class OidcConfigAdminController {
try {
tokenExchanger.invalidateCache();
String authEndpoint = tokenExchanger.getAuthorizationEndpoint();
auditService.log("test_oidc", AuditCategory.CONFIG, "oidc", null, AuditResult.SUCCESS, httpRequest);
return ResponseEntity.ok(new OidcTestResult("ok", authEndpoint));
} catch (Exception e) {
log.warn("OIDC connectivity test failed: {}", e.getMessage());
@@ -125,9 +138,10 @@ public class OidcConfigAdminController {
@DeleteMapping
@Operation(summary = "Delete OIDC configuration")
@ApiResponse(responseCode = "204", description = "Configuration deleted")
public ResponseEntity<Void> deleteConfig() {
public ResponseEntity<Void> deleteConfig(HttpServletRequest httpRequest) {
configRepository.delete();
tokenExchanger.invalidateCache();
auditService.log("delete_oidc", AuditCategory.CONFIG, "oidc", null, AuditResult.SUCCESS, httpRequest);
log.info("OIDC configuration deleted");
return ResponseEntity.noContent().build();
}

View File

@@ -1,11 +1,16 @@
package com.cameleer3.server.app.controller;
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.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.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -15,6 +20,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
* Admin endpoints for user management.
@@ -23,12 +29,15 @@ import java.util.List;
@RestController
@RequestMapping("/api/v1/admin/users")
@Tag(name = "User Admin", description = "User management (ADMIN only)")
@PreAuthorize("hasRole('ADMIN')")
public class UserAdminController {
private final UserRepository userRepository;
private final AuditService auditService;
public UserAdminController(UserRepository userRepository) {
public UserAdminController(UserRepository userRepository, AuditService auditService) {
this.userRepository = userRepository;
this.auditService = auditService;
}
@GetMapping
@@ -53,19 +62,25 @@ public class UserAdminController {
@ApiResponse(responseCode = "200", description = "Roles updated")
@ApiResponse(responseCode = "404", description = "User not found")
public ResponseEntity<Void> updateRoles(@PathVariable String userId,
@RequestBody RolesRequest request) {
@RequestBody RolesRequest request,
HttpServletRequest httpRequest) {
if (userRepository.findById(userId).isEmpty()) {
return ResponseEntity.notFound().build();
}
userRepository.updateRoles(userId, request.roles());
auditService.log("update_roles", AuditCategory.USER_MGMT, userId,
Map.of("roles", request.roles()), AuditResult.SUCCESS, httpRequest);
return ResponseEntity.ok().build();
}
@DeleteMapping("/{userId}")
@Operation(summary = "Delete user")
@ApiResponse(responseCode = "204", description = "User deleted")
public ResponseEntity<Void> deleteUser(@PathVariable String userId) {
public ResponseEntity<Void> deleteUser(@PathVariable String userId,
HttpServletRequest httpRequest) {
userRepository.delete(userId);
auditService.log("delete_user", AuditCategory.USER_MGMT, userId,
null, AuditResult.SUCCESS, httpRequest);
return ResponseEntity.noContent().build();
}