feat(audit): add SOC 2 audit logging to vendor admin, auth policy, email connector, and certificate operations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-29 11:23:13 +02:00
parent 295a185a03
commit 88733d76c0
7 changed files with 138 additions and 36 deletions

View File

@@ -102,15 +102,15 @@ public class CertificateController {
} }
@PostMapping("/activate") @PostMapping("/activate")
public ResponseEntity<Void> activate() { public ResponseEntity<Void> activate(@AuthenticationPrincipal Jwt jwt) {
certificateService.activate(); certificateService.activate(resolveActorId(jwt));
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
@PostMapping("/restore") @PostMapping("/restore")
public ResponseEntity<Void> restore() { public ResponseEntity<Void> restore(@AuthenticationPrincipal Jwt jwt) {
try { try {
certificateService.restore(); certificateService.restore(resolveActorId(jwt));
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
return ResponseEntity.badRequest().body(null); return ResponseEntity.badRequest().body(null);
@@ -118,8 +118,8 @@ public class CertificateController {
} }
@DeleteMapping("/staged") @DeleteMapping("/staged")
public ResponseEntity<Void> discardStaged() { public ResponseEntity<Void> discardStaged(@AuthenticationPrincipal Jwt jwt) {
certificateService.discardStaged(); certificateService.discardStaged(resolveActorId(jwt));
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }

View File

@@ -1,5 +1,7 @@
package io.cameleer.saas.certificate; package io.cameleer.saas.certificate;
import io.cameleer.saas.audit.AuditAction;
import io.cameleer.saas.audit.AuditService;
import io.cameleer.saas.tenant.TenantRepository; import io.cameleer.saas.tenant.TenantRepository;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -8,6 +10,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
@Service @Service
@@ -18,13 +21,16 @@ public class CertificateService {
private final CertificateManager certManager; private final CertificateManager certManager;
private final CertificateRepository certRepository; private final CertificateRepository certRepository;
private final TenantRepository tenantRepository; private final TenantRepository tenantRepository;
private final AuditService auditService;
public CertificateService(CertificateManager certManager, public CertificateService(CertificateManager certManager,
CertificateRepository certRepository, CertificateRepository certRepository,
TenantRepository tenantRepository) { TenantRepository tenantRepository,
AuditService auditService) {
this.certManager = certManager; this.certManager = certManager;
this.certRepository = certRepository; this.certRepository = certRepository;
this.tenantRepository = tenantRepository; this.tenantRepository = tenantRepository;
this.auditService = auditService;
} }
public record CertificateOverview( public record CertificateOverview(
@@ -61,12 +67,17 @@ public class CertificateService {
entity.setUploadedBy(actorId); entity.setUploadedBy(actorId);
certRepository.save(entity); certRepository.save(entity);
auditService.log(actorId, null, null, AuditAction.CERTIFICATE_STAGED, entity.getFingerprint(),
null, null, "SUCCESS",
Map.of("subject", result.info().subject(), "issuer", result.info().issuer(),
"hasCa", result.info().hasCaBundle()));
log.info("Certificate staged by actor {}: subject={}", actorId, result.info().subject()); log.info("Certificate staged by actor {}: subject={}", actorId, result.info().subject());
return result; return result;
} }
@Transactional @Transactional
public void activate() { public void activate(UUID actorId) {
var staged = certRepository.findByStatus(CertificateEntity.Status.STAGED) var staged = certRepository.findByStatus(CertificateEntity.Status.STAGED)
.orElseThrow(() -> new IllegalStateException("No staged certificate to activate")); .orElseThrow(() -> new IllegalStateException("No staged certificate to activate"));
@@ -86,11 +97,14 @@ public class CertificateService {
staged.setActivatedAt(Instant.now()); staged.setActivatedAt(Instant.now());
certRepository.save(staged); certRepository.save(staged);
auditService.log(actorId, null, null, AuditAction.CERTIFICATE_ACTIVATED, staged.getFingerprint(),
null, null, "SUCCESS", Map.of("subject", staged.getSubject()));
log.info("Certificate activated: subject={}", staged.getSubject()); log.info("Certificate activated: subject={}", staged.getSubject());
} }
@Transactional @Transactional
public void restore() { public void restore(UUID actorId) {
var archived = certRepository.findByStatus(CertificateEntity.Status.ARCHIVED) var archived = certRepository.findByStatus(CertificateEntity.Status.ARCHIVED)
.orElseThrow(() -> new IllegalStateException("No archived certificate to restore")); .orElseThrow(() -> new IllegalStateException("No archived certificate to restore"));
@@ -115,13 +129,18 @@ public class CertificateService {
certRepository.save(active); certRepository.save(active);
} }
auditService.log(actorId, null, null, AuditAction.CERTIFICATE_RESTORED, archived.getFingerprint(),
null, null, "SUCCESS", Map.of("subject", archived.getSubject()));
log.info("Certificate restored from archive: subject={}", archived.getSubject()); log.info("Certificate restored from archive: subject={}", archived.getSubject());
} }
@Transactional @Transactional
public void discardStaged() { public void discardStaged(UUID actorId) {
certManager.discardStaged(); certManager.discardStaged();
certRepository.findByStatus(CertificateEntity.Status.STAGED).ifPresent(certRepository::delete); certRepository.findByStatus(CertificateEntity.Status.STAGED).ifPresent(certRepository::delete);
auditService.log(actorId, null, null, AuditAction.CERTIFICATE_DISCARDED, "staged",
null, null, "SUCCESS", null);
log.info("Staged certificate discarded"); log.info("Staged certificate discarded");
} }

View File

@@ -7,6 +7,8 @@ import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
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;
@@ -15,6 +17,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.Map; import java.util.Map;
import java.util.UUID;
@RestController @RestController
@RequestMapping("/api/vendor/email-connector") @RequestMapping("/api/vendor/email-connector")
@@ -76,7 +79,8 @@ public class EmailConnectorController {
} }
@PostMapping @PostMapping
public ResponseEntity<?> save(@Valid @RequestBody SmtpConfigRequest request) { public ResponseEntity<?> save(@AuthenticationPrincipal Jwt jwt,
@Valid @RequestBody SmtpConfigRequest request) {
// Resolve password: use provided value, or fall back to existing password from Logto // Resolve password: use provided value, or fall back to existing password from Logto
String password = request.password(); String password = request.password();
if (password == null || password.isBlank()) { if (password == null || password.isBlank()) {
@@ -98,13 +102,13 @@ public class EmailConnectorController {
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage())); return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
} }
var status = emailConnectorService.saveSmtpConnector(smtp, request.registrationEnabled()); var status = emailConnectorService.saveSmtpConnector(smtp, request.registrationEnabled(), resolveActorId(jwt));
return ResponseEntity.ok(EmailConnectorResponse.from(status)); return ResponseEntity.ok(EmailConnectorResponse.from(status));
} }
@DeleteMapping @DeleteMapping
public ResponseEntity<Void> delete() { public ResponseEntity<Void> delete(@AuthenticationPrincipal Jwt jwt) {
emailConnectorService.deleteEmailConnector(); emailConnectorService.deleteEmailConnector(resolveActorId(jwt));
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
@@ -119,13 +123,22 @@ public class EmailConnectorController {
} }
@PostMapping("/registration") @PostMapping("/registration")
public ResponseEntity<Void> toggleRegistration(@RequestBody Map<String, Boolean> body) { public ResponseEntity<Void> toggleRegistration(@AuthenticationPrincipal Jwt jwt,
@RequestBody Map<String, Boolean> body) {
boolean enabled = body.getOrDefault("enabled", false); boolean enabled = body.getOrDefault("enabled", false);
var existing = emailConnectorService.getEmailConnector(); var existing = emailConnectorService.getEmailConnector();
if (existing == null && enabled) { if (existing == null && enabled) {
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
emailConnectorService.setRegistrationEnabled(enabled); emailConnectorService.setRegistrationEnabled(enabled, resolveActorId(jwt));
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
private UUID resolveActorId(Jwt jwt) {
try {
return UUID.fromString(jwt.getSubject());
} catch (Exception e) {
return UUID.nameUUIDFromBytes(jwt.getSubject().getBytes());
}
}
} }

View File

@@ -1,5 +1,7 @@
package io.cameleer.saas.vendor; package io.cameleer.saas.vendor;
import io.cameleer.saas.audit.AuditAction;
import io.cameleer.saas.audit.AuditService;
import io.cameleer.saas.identity.LogtoManagementClient; import io.cameleer.saas.identity.LogtoManagementClient;
import io.cameleer.saas.provisioning.ProvisioningProperties; import io.cameleer.saas.provisioning.ProvisioningProperties;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -14,6 +16,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.UUID;
@Service @Service
public class EmailConnectorService { public class EmailConnectorService {
@@ -23,10 +26,14 @@ public class EmailConnectorService {
private final LogtoManagementClient logtoClient; private final LogtoManagementClient logtoClient;
private final ProvisioningProperties provisioningProps; private final ProvisioningProperties provisioningProps;
private final AuditService auditService;
public EmailConnectorService(LogtoManagementClient logtoClient, ProvisioningProperties provisioningProps) { public EmailConnectorService(LogtoManagementClient logtoClient,
ProvisioningProperties provisioningProps,
AuditService auditService) {
this.logtoClient = logtoClient; this.logtoClient = logtoClient;
this.provisioningProps = provisioningProps; this.provisioningProps = provisioningProps;
this.auditService = auditService;
} }
public record SmtpConfig(String host, int port, String username, String password, String fromEmail) {} public record SmtpConfig(String host, int port, String username, String password, String fromEmail) {}
@@ -120,7 +127,7 @@ public class EmailConnectorService {
} }
/** Create or update the SMTP email connector. Returns the connector status. */ /** Create or update the SMTP email connector. Returns the connector status. */
public EmailConnectorStatus saveSmtpConnector(SmtpConfig smtp, Boolean registrationEnabled) { public EmailConnectorStatus saveSmtpConnector(SmtpConfig smtp, Boolean registrationEnabled, UUID actorId) {
var connectorConfig = buildSmtpConfig(smtp); var connectorConfig = buildSmtpConfig(smtp);
// Check if an email connector already exists // Check if an email connector already exists
@@ -133,20 +140,24 @@ public class EmailConnectorService {
log.info("Created SMTP email connector: {}", result != null ? result.get("id") : "unknown"); log.info("Created SMTP email connector: {}", result != null ? result.get("id") : "unknown");
} }
auditService.log(actorId, null, null, AuditAction.EMAIL_CONNECTOR_SAVED, null, null, null, "SUCCESS",
Map.of("host", smtp.host(), "port", smtp.port(), "fromEmail", smtp.fromEmail()));
// Handle registration toggle // Handle registration toggle
boolean enableReg = registrationEnabled != null ? registrationEnabled : (existing == null); boolean enableReg = registrationEnabled != null ? registrationEnabled : (existing == null);
setRegistrationEnabled(enableReg); setRegistrationEnabled(enableReg, actorId);
return getEmailConnector(); return getEmailConnector();
} }
/** Delete the email connector and disable registration. */ /** Delete the email connector and disable registration. */
public void deleteEmailConnector() { public void deleteEmailConnector(UUID actorId) {
var existing = getEmailConnector(); var existing = getEmailConnector();
if (existing != null) { if (existing != null) {
logtoClient.deleteConnector(existing.connectorId()); logtoClient.deleteConnector(existing.connectorId());
setRegistrationEnabled(false);
log.info("Deleted email connector: {}", existing.connectorId()); log.info("Deleted email connector: {}", existing.connectorId());
auditService.log(actorId, null, null, AuditAction.EMAIL_CONNECTOR_DELETED, null, null, null, "SUCCESS", null);
setRegistrationEnabled(false, actorId);
} }
} }
@@ -169,7 +180,7 @@ public class EmailConnectorService {
} }
/** Set registration mode on the Logto sign-in experience. */ /** Set registration mode on the Logto sign-in experience. */
public void setRegistrationEnabled(boolean enabled) { public void setRegistrationEnabled(boolean enabled, UUID actorId) {
if (enabled) { if (enabled) {
logtoClient.updateSignInExperience(Map.of( logtoClient.updateSignInExperience(Map.of(
"signInMode", "SignInAndRegister", "signInMode", "SignInAndRegister",
@@ -201,6 +212,8 @@ public class EmailConnectorService {
) )
)); ));
} }
auditService.log(actorId, null, null, AuditAction.REGISTRATION_TOGGLED, null, null, null, "SUCCESS",
Map.of("enabled", enabled));
} }
/** Check if registration is currently enabled in Logto. */ /** Check if registration is currently enabled in Logto. */

View File

@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID;
@RestController @RestController
@RequestMapping("/api/vendor/admins") @RequestMapping("/api/vendor/admins")
@@ -27,27 +28,38 @@ public class VendorAdminController {
} }
@PostMapping @PostMapping
public CreateAdminResponse createAdmin(@RequestBody CreateAdminRequest request) { public CreateAdminResponse createAdmin(@AuthenticationPrincipal Jwt jwt,
return vendorAdminService.createAdmin(request); @RequestBody CreateAdminRequest request) {
return vendorAdminService.createAdmin(request, resolveActorId(jwt));
} }
@DeleteMapping("/{userId}") @DeleteMapping("/{userId}")
public ResponseEntity<Void> removeAdmin(@AuthenticationPrincipal Jwt jwt, public ResponseEntity<Void> removeAdmin(@AuthenticationPrincipal Jwt jwt,
@PathVariable String userId) { @PathVariable String userId) {
vendorAdminService.removeAdmin(userId, jwt.getSubject()); vendorAdminService.removeAdmin(userId, jwt.getSubject(), resolveActorId(jwt));
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
@PostMapping("/{userId}/reset-password") @PostMapping("/{userId}/reset-password")
public ResponseEntity<Void> resetPassword(@PathVariable String userId, public ResponseEntity<Void> resetPassword(@AuthenticationPrincipal Jwt jwt,
@PathVariable String userId,
@RequestBody Map<String, String> body) { @RequestBody Map<String, String> body) {
vendorAdminService.resetAdminPassword(userId, body.get("password")); vendorAdminService.resetAdminPassword(userId, body.get("password"), resolveActorId(jwt));
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
@DeleteMapping("/{userId}/mfa") @DeleteMapping("/{userId}/mfa")
public ResponseEntity<Void> resetMfa(@PathVariable String userId) { public ResponseEntity<Void> resetMfa(@AuthenticationPrincipal Jwt jwt,
vendorAdminService.resetAdminMfa(userId); @PathVariable String userId) {
vendorAdminService.resetAdminMfa(userId, resolveActorId(jwt));
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
private UUID resolveActorId(Jwt jwt) {
try {
return UUID.fromString(jwt.getSubject());
} catch (Exception e) {
return UUID.nameUUIDFromBytes(jwt.getSubject().getBytes());
}
}
} }

View File

@@ -1,6 +1,8 @@
package io.cameleer.saas.vendor; package io.cameleer.saas.vendor;
import io.cameleer.saas.account.AccountService; import io.cameleer.saas.account.AccountService;
import io.cameleer.saas.audit.AuditAction;
import io.cameleer.saas.audit.AuditService;
import io.cameleer.saas.identity.LogtoManagementClient; import io.cameleer.saas.identity.LogtoManagementClient;
import io.cameleer.saas.notification.PasswordResetNotificationService; import io.cameleer.saas.notification.PasswordResetNotificationService;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -11,6 +13,7 @@ import org.springframework.web.server.ResponseStatusException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID;
@Service @Service
public class VendorAdminService { public class VendorAdminService {
@@ -22,15 +25,18 @@ public class VendorAdminService {
private final AccountService accountService; private final AccountService accountService;
private final EmailConnectorService emailConnectorService; private final EmailConnectorService emailConnectorService;
private final PasswordResetNotificationService passwordNotificationService; private final PasswordResetNotificationService passwordNotificationService;
private final AuditService auditService;
public VendorAdminService(LogtoManagementClient logtoClient, public VendorAdminService(LogtoManagementClient logtoClient,
AccountService accountService, AccountService accountService,
EmailConnectorService emailConnectorService, EmailConnectorService emailConnectorService,
PasswordResetNotificationService passwordNotificationService) { PasswordResetNotificationService passwordNotificationService,
AuditService auditService) {
this.logtoClient = logtoClient; this.logtoClient = logtoClient;
this.accountService = accountService; this.accountService = accountService;
this.emailConnectorService = emailConnectorService; this.emailConnectorService = emailConnectorService;
this.passwordNotificationService = passwordNotificationService; this.passwordNotificationService = passwordNotificationService;
this.auditService = auditService;
} }
// --- Records --- // --- Records ---
@@ -62,7 +68,7 @@ public class VendorAdminService {
.toList(); .toList();
} }
public CreateAdminResponse createAdmin(CreateAdminRequest request) { public CreateAdminResponse createAdmin(CreateAdminRequest request, UUID actorId) {
if (request.email() == null || request.email().isBlank() || !request.email().contains("@")) { if (request.email() == null || request.email().isBlank() || !request.email().contains("@")) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Valid email address required"); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Valid email address required");
} }
@@ -97,24 +103,28 @@ public class VendorAdminService {
logtoClient.assignGlobalRole(userId, roleId); logtoClient.assignGlobalRole(userId, roleId);
log.info("Assigned vendor role to user {}", userId); log.info("Assigned vendor role to user {}", userId);
auditService.log(actorId, null, null, AuditAction.ADMIN_CREATED, userId, null, null, "SUCCESS",
Map.of("email", request.email(), "invited", invited));
return new CreateAdminResponse(invited, invited ? null : tempPassword); return new CreateAdminResponse(invited, invited ? null : tempPassword);
} }
public void removeAdmin(String userId, String requesterId) { public void removeAdmin(String userId, String requesterId, UUID actorId) {
if (userId.equals(requesterId)) { if (userId.equals(requesterId)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot remove yourself as administrator"); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot remove yourself as administrator");
} }
String roleId = getVendorRoleId(); String roleId = getVendorRoleId();
logtoClient.revokeGlobalRole(userId, roleId); logtoClient.revokeGlobalRole(userId, roleId);
log.info("Revoked vendor role from user {}", userId); log.info("Revoked vendor role from user {}", userId);
auditService.log(actorId, null, null, AuditAction.ADMIN_REMOVED, userId, null, null, "SUCCESS", null);
} }
public void resetAdminPassword(String userId, String newPassword) { public void resetAdminPassword(String userId, String newPassword, UUID actorId) {
verifyIsVendorAdmin(userId); verifyIsVendorAdmin(userId);
accountService.validatePassword(newPassword); accountService.validatePassword(newPassword);
logtoClient.updateUserPassword(userId, newPassword); logtoClient.updateUserPassword(userId, newPassword);
log.info("Reset password for vendor admin {}", userId); log.info("Reset password for vendor admin {}", userId);
auditService.log(actorId, null, null, AuditAction.ADMIN_PASSWORD_RESET, userId, null, null, "SUCCESS", null);
// Send notification email // Send notification email
try { try {
@@ -130,10 +140,11 @@ public class VendorAdminService {
} }
} }
public void resetAdminMfa(String userId) { public void resetAdminMfa(String userId, UUID actorId) {
verifyIsVendorAdmin(userId); verifyIsVendorAdmin(userId);
logtoClient.deleteAllMfaVerifications(userId); logtoClient.deleteAllMfaVerifications(userId);
log.info("Reset MFA for vendor admin {}", userId); log.info("Reset MFA for vendor admin {}", userId);
auditService.log(actorId, null, null, AuditAction.ADMIN_MFA_RESET, userId, null, null, "SUCCESS", null);
} }
private void verifyIsVendorAdmin(String userId) { private void verifyIsVendorAdmin(String userId) {

View File

@@ -1,10 +1,17 @@
package io.cameleer.saas.vendor; package io.cameleer.saas.vendor;
import io.cameleer.saas.audit.AuditAction;
import io.cameleer.saas.audit.AuditService;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID;
@RestController @RestController
@RequestMapping("/api/vendor/auth-policy") @RequestMapping("/api/vendor/auth-policy")
@@ -15,9 +22,12 @@ public class VendorAuthPolicyController {
private static final Set<String> VALID_PASSKEY_MODES = Set.of("optional", "preferred", "required"); private static final Set<String> VALID_PASSKEY_MODES = Set.of("optional", "preferred", "required");
private final VendorAuthPolicyRepository repository; private final VendorAuthPolicyRepository repository;
private final AuditService auditService;
public VendorAuthPolicyController(VendorAuthPolicyRepository repository) { public VendorAuthPolicyController(VendorAuthPolicyRepository repository,
AuditService auditService) {
this.repository = repository; this.repository = repository;
this.auditService = auditService;
} }
public record AuthPolicyResponse(String mfaMode, boolean passkeyEnabled, String passkeyMode) { public record AuthPolicyResponse(String mfaMode, boolean passkeyEnabled, String passkeyMode) {
@@ -34,9 +44,14 @@ public class VendorAuthPolicyController {
} }
@PutMapping @PutMapping
public ResponseEntity<AuthPolicyResponse> updatePolicy(@RequestBody AuthPolicyUpdateRequest request) { public ResponseEntity<AuthPolicyResponse> updatePolicy(@AuthenticationPrincipal Jwt jwt,
@RequestBody AuthPolicyUpdateRequest request) {
var policy = repository.getPolicy(); var policy = repository.getPolicy();
String mfaMode_old = policy.getMfaMode();
boolean passkeyEnabled_old = policy.isPasskeyEnabled();
String passkeyMode_old = policy.getPasskeyMode();
if (request.mfaMode() != null) { if (request.mfaMode() != null) {
if (!VALID_MFA_MODES.contains(request.mfaMode())) { if (!VALID_MFA_MODES.contains(request.mfaMode())) {
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
@@ -54,6 +69,25 @@ public class VendorAuthPolicyController {
} }
repository.save(policy); repository.save(policy);
var changes = new HashMap<String, Object>();
changes.put("mfaMode_old", mfaMode_old);
changes.put("mfaMode_new", policy.getMfaMode());
changes.put("passkeyEnabled_old", passkeyEnabled_old);
changes.put("passkeyEnabled_new", policy.isPasskeyEnabled());
changes.put("passkeyMode_old", passkeyMode_old);
changes.put("passkeyMode_new", policy.getPasskeyMode());
auditService.log(resolveActorId(jwt), null, null, AuditAction.PLATFORM_AUTH_POLICY_UPDATED,
null, null, null, "SUCCESS", changes);
return ResponseEntity.ok(AuthPolicyResponse.from(policy)); return ResponseEntity.ok(AuthPolicyResponse.from(policy));
} }
private UUID resolveActorId(Jwt jwt) {
try {
return UUID.fromString(jwt.getSubject());
} catch (Exception e) {
return UUID.nameUUIDFromBytes(jwt.getSubject().getBytes());
}
}
} }