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:
@@ -1,5 +1,7 @@
|
|||||||
package com.cameleer3.server.app.config;
|
package com.cameleer3.server.app.config;
|
||||||
|
|
||||||
|
import com.cameleer3.server.core.admin.AuditRepository;
|
||||||
|
import com.cameleer3.server.core.admin.AuditService;
|
||||||
import com.cameleer3.server.core.detail.DetailService;
|
import com.cameleer3.server.core.detail.DetailService;
|
||||||
import com.cameleer3.server.core.indexing.SearchIndexer;
|
import com.cameleer3.server.core.indexing.SearchIndexer;
|
||||||
import com.cameleer3.server.core.ingestion.IngestionService;
|
import com.cameleer3.server.core.ingestion.IngestionService;
|
||||||
@@ -25,6 +27,11 @@ public class StorageBeanConfig {
|
|||||||
return new SearchIndexer(executionStore, searchIndex, debounceMs, queueSize);
|
return new SearchIndexer(executionStore, searchIndex, debounceMs, queueSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public AuditService auditService(AuditRepository auditRepository) {
|
||||||
|
return new AuditService(auditRepository);
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public IngestionService ingestionService(ExecutionStore executionStore,
|
public IngestionService ingestionService(ExecutionStore executionStore,
|
||||||
DiagramStore diagramStore,
|
DiagramStore diagramStore,
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ import com.cameleer3.server.app.dto.OidcAdminConfigRequest;
|
|||||||
import com.cameleer3.server.app.dto.OidcAdminConfigResponse;
|
import com.cameleer3.server.app.dto.OidcAdminConfigResponse;
|
||||||
import com.cameleer3.server.app.dto.OidcTestResult;
|
import com.cameleer3.server.app.dto.OidcTestResult;
|
||||||
import com.cameleer3.server.app.security.OidcTokenExchanger;
|
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.OidcConfig;
|
||||||
import com.cameleer3.server.core.security.OidcConfigRepository;
|
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.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
@@ -16,6 +20,7 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
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.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
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 org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,17 +41,21 @@ import java.util.Optional;
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/admin/oidc")
|
@RequestMapping("/api/v1/admin/oidc")
|
||||||
@Tag(name = "OIDC Config Admin", description = "OIDC provider configuration (ADMIN only)")
|
@Tag(name = "OIDC Config Admin", description = "OIDC provider configuration (ADMIN only)")
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
public class OidcConfigAdminController {
|
public class OidcConfigAdminController {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(OidcConfigAdminController.class);
|
private static final Logger log = LoggerFactory.getLogger(OidcConfigAdminController.class);
|
||||||
|
|
||||||
private final OidcConfigRepository configRepository;
|
private final OidcConfigRepository configRepository;
|
||||||
private final OidcTokenExchanger tokenExchanger;
|
private final OidcTokenExchanger tokenExchanger;
|
||||||
|
private final AuditService auditService;
|
||||||
|
|
||||||
public OidcConfigAdminController(OidcConfigRepository configRepository,
|
public OidcConfigAdminController(OidcConfigRepository configRepository,
|
||||||
OidcTokenExchanger tokenExchanger) {
|
OidcTokenExchanger tokenExchanger,
|
||||||
|
AuditService auditService) {
|
||||||
this.configRepository = configRepository;
|
this.configRepository = configRepository;
|
||||||
this.tokenExchanger = tokenExchanger;
|
this.tokenExchanger = tokenExchanger;
|
||||||
|
this.auditService = auditService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@@ -64,7 +74,8 @@ public class OidcConfigAdminController {
|
|||||||
@ApiResponse(responseCode = "200", description = "Configuration saved")
|
@ApiResponse(responseCode = "200", description = "Configuration saved")
|
||||||
@ApiResponse(responseCode = "400", description = "Invalid configuration",
|
@ApiResponse(responseCode = "400", description = "Invalid configuration",
|
||||||
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
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
|
// Resolve client_secret: if masked or empty, preserve existing
|
||||||
String clientSecret = request.clientSecret();
|
String clientSecret = request.clientSecret();
|
||||||
if (clientSecret == null || clientSecret.isBlank() || clientSecret.equals("********")) {
|
if (clientSecret == null || clientSecret.isBlank() || clientSecret.equals("********")) {
|
||||||
@@ -95,6 +106,7 @@ public class OidcConfigAdminController {
|
|||||||
configRepository.save(config);
|
configRepository.save(config);
|
||||||
tokenExchanger.invalidateCache();
|
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());
|
log.info("OIDC configuration updated: enabled={}, issuer={}", config.enabled(), config.issuerUri());
|
||||||
return ResponseEntity.ok(OidcAdminConfigResponse.from(config));
|
return ResponseEntity.ok(OidcAdminConfigResponse.from(config));
|
||||||
}
|
}
|
||||||
@@ -104,7 +116,7 @@ public class OidcConfigAdminController {
|
|||||||
@ApiResponse(responseCode = "200", description = "Provider reachable")
|
@ApiResponse(responseCode = "200", description = "Provider reachable")
|
||||||
@ApiResponse(responseCode = "400", description = "Provider unreachable or misconfigured",
|
@ApiResponse(responseCode = "400", description = "Provider unreachable or misconfigured",
|
||||||
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||||
public ResponseEntity<OidcTestResult> testConnection() {
|
public ResponseEntity<OidcTestResult> testConnection(HttpServletRequest httpRequest) {
|
||||||
Optional<OidcConfig> config = configRepository.find();
|
Optional<OidcConfig> config = configRepository.find();
|
||||||
if (config.isEmpty() || !config.get().enabled()) {
|
if (config.isEmpty() || !config.get().enabled()) {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||||
@@ -114,6 +126,7 @@ public class OidcConfigAdminController {
|
|||||||
try {
|
try {
|
||||||
tokenExchanger.invalidateCache();
|
tokenExchanger.invalidateCache();
|
||||||
String authEndpoint = tokenExchanger.getAuthorizationEndpoint();
|
String authEndpoint = tokenExchanger.getAuthorizationEndpoint();
|
||||||
|
auditService.log("test_oidc", AuditCategory.CONFIG, "oidc", null, AuditResult.SUCCESS, httpRequest);
|
||||||
return ResponseEntity.ok(new OidcTestResult("ok", authEndpoint));
|
return ResponseEntity.ok(new OidcTestResult("ok", authEndpoint));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("OIDC connectivity test failed: {}", e.getMessage());
|
log.warn("OIDC connectivity test failed: {}", e.getMessage());
|
||||||
@@ -125,9 +138,10 @@ public class OidcConfigAdminController {
|
|||||||
@DeleteMapping
|
@DeleteMapping
|
||||||
@Operation(summary = "Delete OIDC configuration")
|
@Operation(summary = "Delete OIDC configuration")
|
||||||
@ApiResponse(responseCode = "204", description = "Configuration deleted")
|
@ApiResponse(responseCode = "204", description = "Configuration deleted")
|
||||||
public ResponseEntity<Void> deleteConfig() {
|
public ResponseEntity<Void> deleteConfig(HttpServletRequest httpRequest) {
|
||||||
configRepository.delete();
|
configRepository.delete();
|
||||||
tokenExchanger.invalidateCache();
|
tokenExchanger.invalidateCache();
|
||||||
|
auditService.log("delete_oidc", AuditCategory.CONFIG, "oidc", null, AuditResult.SUCCESS, httpRequest);
|
||||||
log.info("OIDC configuration deleted");
|
log.info("OIDC configuration deleted");
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
package com.cameleer3.server.app.controller;
|
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.UserInfo;
|
||||||
import com.cameleer3.server.core.security.UserRepository;
|
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.Operation;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import org.springframework.http.ResponseEntity;
|
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.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
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 org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Admin endpoints for user management.
|
* Admin endpoints for user management.
|
||||||
@@ -23,12 +29,15 @@ import java.util.List;
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/admin/users")
|
@RequestMapping("/api/v1/admin/users")
|
||||||
@Tag(name = "User Admin", description = "User management (ADMIN only)")
|
@Tag(name = "User Admin", description = "User management (ADMIN only)")
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
public class UserAdminController {
|
public class UserAdminController {
|
||||||
|
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
private final AuditService auditService;
|
||||||
|
|
||||||
public UserAdminController(UserRepository userRepository) {
|
public UserAdminController(UserRepository userRepository, AuditService auditService) {
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
|
this.auditService = auditService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@@ -53,19 +62,25 @@ public class UserAdminController {
|
|||||||
@ApiResponse(responseCode = "200", description = "Roles updated")
|
@ApiResponse(responseCode = "200", description = "Roles updated")
|
||||||
@ApiResponse(responseCode = "404", description = "User not found")
|
@ApiResponse(responseCode = "404", description = "User not found")
|
||||||
public ResponseEntity<Void> updateRoles(@PathVariable String userId,
|
public ResponseEntity<Void> updateRoles(@PathVariable String userId,
|
||||||
@RequestBody RolesRequest request) {
|
@RequestBody RolesRequest request,
|
||||||
|
HttpServletRequest httpRequest) {
|
||||||
if (userRepository.findById(userId).isEmpty()) {
|
if (userRepository.findById(userId).isEmpty()) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
userRepository.updateRoles(userId, request.roles());
|
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();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{userId}")
|
@DeleteMapping("/{userId}")
|
||||||
@Operation(summary = "Delete user")
|
@Operation(summary = "Delete user")
|
||||||
@ApiResponse(responseCode = "204", description = "User deleted")
|
@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);
|
userRepository.delete(userId);
|
||||||
|
auditService.log("delete_user", AuditCategory.USER_MGMT, userId,
|
||||||
|
null, AuditResult.SUCCESS, httpRequest);
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,15 @@ package com.cameleer3.server.app.security;
|
|||||||
import com.cameleer3.server.app.dto.AuthTokenResponse;
|
import com.cameleer3.server.app.dto.AuthTokenResponse;
|
||||||
import com.cameleer3.server.app.dto.ErrorResponse;
|
import com.cameleer3.server.app.dto.ErrorResponse;
|
||||||
import com.cameleer3.server.app.dto.OidcPublicConfigResponse;
|
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.JwtService;
|
||||||
import com.cameleer3.server.core.security.OidcConfig;
|
import com.cameleer3.server.core.security.OidcConfig;
|
||||||
import com.cameleer3.server.core.security.OidcConfigRepository;
|
import com.cameleer3.server.core.security.OidcConfigRepository;
|
||||||
import com.cameleer3.server.core.security.UserInfo;
|
import com.cameleer3.server.core.security.UserInfo;
|
||||||
import com.cameleer3.server.core.security.UserRepository;
|
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.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
@@ -27,6 +31,7 @@ import org.springframework.web.server.ResponseStatusException;
|
|||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,15 +51,18 @@ public class OidcAuthController {
|
|||||||
private final OidcConfigRepository configRepository;
|
private final OidcConfigRepository configRepository;
|
||||||
private final JwtService jwtService;
|
private final JwtService jwtService;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
private final AuditService auditService;
|
||||||
|
|
||||||
public OidcAuthController(OidcTokenExchanger tokenExchanger,
|
public OidcAuthController(OidcTokenExchanger tokenExchanger,
|
||||||
OidcConfigRepository configRepository,
|
OidcConfigRepository configRepository,
|
||||||
JwtService jwtService,
|
JwtService jwtService,
|
||||||
UserRepository userRepository) {
|
UserRepository userRepository,
|
||||||
|
AuditService auditService) {
|
||||||
this.tokenExchanger = tokenExchanger;
|
this.tokenExchanger = tokenExchanger;
|
||||||
this.configRepository = configRepository;
|
this.configRepository = configRepository;
|
||||||
this.jwtService = jwtService;
|
this.jwtService = jwtService;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
|
this.auditService = auditService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,7 +108,8 @@ public class OidcAuthController {
|
|||||||
@ApiResponse(responseCode = "403", description = "Account not provisioned",
|
@ApiResponse(responseCode = "403", description = "Account not provisioned",
|
||||||
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||||
@ApiResponse(responseCode = "404", description = "OIDC not configured or disabled")
|
@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();
|
Optional<OidcConfig> config = configRepository.find();
|
||||||
if (config.isEmpty() || !config.get().enabled()) {
|
if (config.isEmpty() || !config.get().enabled()) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
@@ -132,6 +141,8 @@ public class OidcAuthController {
|
|||||||
|
|
||||||
String displayName = oidcUser.name() != null && !oidcUser.name().isBlank()
|
String displayName = oidcUser.name() != null && !oidcUser.name().isBlank()
|
||||||
? oidcUser.name() : oidcUser.email();
|
? 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()));
|
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken, displayName, oidcUser.idToken()));
|
||||||
} catch (ResponseStatusException e) {
|
} catch (ResponseStatusException e) {
|
||||||
throw e;
|
throw e;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import org.springframework.context.annotation.Bean;
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.HttpStatus;
|
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.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
@@ -27,6 +28,7 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
|
@EnableMethodSecurity
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ package com.cameleer3.server.app.security;
|
|||||||
|
|
||||||
import com.cameleer3.server.app.dto.AuthTokenResponse;
|
import com.cameleer3.server.app.dto.AuthTokenResponse;
|
||||||
import com.cameleer3.server.app.dto.ErrorResponse;
|
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 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.JwtService.JwtValidationResult;
|
||||||
import com.cameleer3.server.core.security.UserInfo;
|
import com.cameleer3.server.core.security.UserInfo;
|
||||||
import com.cameleer3.server.core.security.UserRepository;
|
import com.cameleer3.server.core.security.UserRepository;
|
||||||
@@ -23,6 +27,7 @@ import org.springframework.web.server.ResponseStatusException;
|
|||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authentication endpoints for the UI (local credentials).
|
* Authentication endpoints for the UI (local credentials).
|
||||||
@@ -41,12 +46,14 @@ public class UiAuthController {
|
|||||||
private final JwtService jwtService;
|
private final JwtService jwtService;
|
||||||
private final SecurityProperties properties;
|
private final SecurityProperties properties;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
private final AuditService auditService;
|
||||||
|
|
||||||
public UiAuthController(JwtService jwtService, SecurityProperties properties,
|
public UiAuthController(JwtService jwtService, SecurityProperties properties,
|
||||||
UserRepository userRepository) {
|
UserRepository userRepository, AuditService auditService) {
|
||||||
this.jwtService = jwtService;
|
this.jwtService = jwtService;
|
||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
|
this.auditService = auditService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
@@ -54,19 +61,24 @@ public class UiAuthController {
|
|||||||
@ApiResponse(responseCode = "200", description = "Login successful")
|
@ApiResponse(responseCode = "200", description = "Login successful")
|
||||||
@ApiResponse(responseCode = "401", description = "Invalid credentials",
|
@ApiResponse(responseCode = "401", description = "Invalid credentials",
|
||||||
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
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 configuredUser = properties.getUiUser();
|
||||||
String configuredPassword = properties.getUiPassword();
|
String configuredPassword = properties.getUiPassword();
|
||||||
|
|
||||||
if (configuredUser == null || configuredUser.isBlank()
|
if (configuredUser == null || configuredUser.isBlank()
|
||||||
|| configuredPassword == null || configuredPassword.isBlank()) {
|
|| configuredPassword == null || configuredPassword.isBlank()) {
|
||||||
log.warn("UI authentication attempted but CAMELEER_UI_USER / CAMELEER_UI_PASSWORD not configured");
|
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");
|
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "UI authentication not configured");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!configuredUser.equals(request.username())
|
if (!configuredUser.equals(request.username())
|
||||||
|| !configuredPassword.equals(request.password())) {
|
|| !configuredPassword.equals(request.password())) {
|
||||||
log.debug("UI login failed for user: {}", request.username());
|
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");
|
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +96,7 @@ public class UiAuthController {
|
|||||||
String accessToken = jwtService.createAccessToken(subject, "user", roles);
|
String accessToken = jwtService.createAccessToken(subject, "user", roles);
|
||||||
String refreshToken = jwtService.createRefreshToken(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());
|
log.info("UI user logged in: {}", request.username());
|
||||||
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken, request.username(), null));
|
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken, request.username(), null));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user