From 372d00809b754d9fd7c349a041ebe30e6bfcc31f Mon Sep 17 00:00:00 2001 From: chenyuan Date: Tue, 4 Aug 2026 13:22:29 +0800 Subject: [PATCH] fix: require issuance audit claims --- .../CommercialBankIssuanceController.java | 11 +++++--- .../security/application/JwtTokenService.java | 1 + .../rest/IssuanceControllerTest.java | 25 +++++++++++++------ 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/yau/digitalrmb/issuance/interfaces/rest/CommercialBankIssuanceController.java b/src/main/java/com/yau/digitalrmb/issuance/interfaces/rest/CommercialBankIssuanceController.java index c9d1af5..c23c9f0 100644 --- a/src/main/java/com/yau/digitalrmb/issuance/interfaces/rest/CommercialBankIssuanceController.java +++ b/src/main/java/com/yau/digitalrmb/issuance/interfaces/rest/CommercialBankIssuanceController.java @@ -14,6 +14,8 @@ import com.yau.digitalrmb.issuance.interfaces.dto.DenominationItemRequest; import com.yau.digitalrmb.issuance.interfaces.dto.DenominationPlanRequest; import com.yau.digitalrmb.issuance.interfaces.dto.UpdateIssuanceRequest; import com.yau.digitalrmb.shared.api.ApiResponse; +import com.yau.digitalrmb.shared.api.ErrorCode; +import com.yau.digitalrmb.shared.exception.BusinessException; import com.yau.digitalrmb.shared.web.TraceIdFilter; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -65,11 +67,12 @@ public class CommercialBankIssuanceController { } private IssuanceAuditActor actor(Jwt jwt){ String username=jwt.getClaimAsString("preferred_username"); - if(username==null||username.trim().isEmpty()) username=jwt.getSubject(); + if(username==null||username.trim().isEmpty()) throw missingAuditClaim("preferred_username"); Object claim=jwt.getClaim("userId"); - String userId=claim==null?jwt.getSubject():String.valueOf(claim); - try{return new IssuanceAuditActor(Long.parseLong(userId),username);} - catch(NumberFormatException exception){throw new IllegalArgumentException("current user id must be a number");} + if(claim==null) throw missingAuditClaim("userId"); + try{return new IssuanceAuditActor(Long.parseLong(String.valueOf(claim)),username);} + catch(NumberFormatException exception){throw new BusinessException(ErrorCode.UNAUTHORIZED,"current user id must be a number");} } + private BusinessException missingAuditClaim(String claim){return new BusinessException(ErrorCode.UNAUTHORIZED,"JWT missing required claim: "+claim);} private ApiResponse ok(T data){return ApiResponse.success(data,MDC.get(TraceIdFilter.MDC_KEY));} } diff --git a/src/main/java/com/yau/digitalrmb/security/application/JwtTokenService.java b/src/main/java/com/yau/digitalrmb/security/application/JwtTokenService.java index ef35c26..7f3237c 100644 --- a/src/main/java/com/yau/digitalrmb/security/application/JwtTokenService.java +++ b/src/main/java/com/yau/digitalrmb/security/application/JwtTokenService.java @@ -42,6 +42,7 @@ public class JwtTokenService { .subject(String.valueOf(platformUserId)) .issuedAt(issuedAt) .expiresAt(expiresAt) + .claim("userId", platformUserId) .claim("preferred_username", account) .claim("roles", authorities) .build(); diff --git a/src/test/java/com/yau/digitalrmb/issuance/interfaces/rest/IssuanceControllerTest.java b/src/test/java/com/yau/digitalrmb/issuance/interfaces/rest/IssuanceControllerTest.java index 51d4107..9823ca4 100644 --- a/src/test/java/com/yau/digitalrmb/issuance/interfaces/rest/IssuanceControllerTest.java +++ b/src/test/java/com/yau/digitalrmb/issuance/interfaces/rest/IssuanceControllerTest.java @@ -29,22 +29,22 @@ class IssuanceControllerTest { void commercialBankWorkflowIsVisibleToCentralBankEnd() throws Exception { String requestBody = "{\"bankCode\":\"BKCHCNBJ00001\",\"organizationId\":\"ORG_3A4B5C6D7E8F\",\"totalAmount\":50000.00,\"currency\":\"DC\",\"denominations\":[{\"denomination\":100,\"quantity\":400},{\"denomination\":50,\"quantity\":200}]}"; String createResponse = mockMvc.perform(post("/api/v1/commercial-banks/issuance/requests") - .with(jwt().jwt(jwt -> jwt.subject("487").claim("preferred_username", "tzs001"))) + .with(jwt().jwt(jwt -> jwt.subject("487").claim("userId", 487L).claim("preferred_username", "tzs001"))) .contentType(MediaType.APPLICATION_JSON).content(requestBody)) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("DRAFT")) .andReturn().getResponse().getContentAsString(); String requestId = JsonPath.read(createResponse, "$.data.id"); String base = "/api/v1/commercial-banks/issuance/requests/" + requestId; - mockMvc.perform(post(base + "/prepare-message").with(jwt().jwt(jwt -> jwt.subject("487").claim("preferred_username", "tzs001")))) + mockMvc.perform(post(base + "/prepare-message").with(jwt().jwt(jwt -> jwt.subject("487").claim("userId", 487L).claim("preferred_username", "tzs001")))) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("MESSAGE_PREPARED")); - mockMvc.perform(post(base + "/digest").with(jwt().jwt(jwt -> jwt.subject("487").claim("preferred_username", "tzs001")))) + mockMvc.perform(post(base + "/digest").with(jwt().jwt(jwt -> jwt.subject("487").claim("userId", 487L).claim("preferred_username", "tzs001")))) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("DIGESTED")); - mockMvc.perform(post(base + "/sign").with(jwt().jwt(jwt -> jwt.subject("487").claim("preferred_username", "tzs001")))) + mockMvc.perform(post(base + "/sign").with(jwt().jwt(jwt -> jwt.subject("487").claim("userId", 487L).claim("preferred_username", "tzs001")))) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("SIGNED")); - mockMvc.perform(post(base + "/package").with(jwt().jwt(jwt -> jwt.subject("487").claim("preferred_username", "tzs001")))) + mockMvc.perform(post(base + "/package").with(jwt().jwt(jwt -> jwt.subject("487").claim("userId", 487L).claim("preferred_username", "tzs001")))) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("PACKAGED")); - mockMvc.perform(post(base + "/send").with(jwt().jwt(jwt -> jwt.subject("487").claim("preferred_username", "tzs001")))) + mockMvc.perform(post(base + "/send").with(jwt().jwt(jwt -> jwt.subject("487").claim("userId", 487L).claim("preferred_username", "tzs001")))) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("SENT")); mockMvc.perform(get("/api/v1/central-banks/issuance/requests/{id}", requestId) @@ -81,7 +81,7 @@ class IssuanceControllerTest { String requestBody = "{\"totalAmount\":50000.00,\"items\":[{\"denomination\":100,\"quantity\":400},{\"denomination\":50,\"quantity\":100},{\"denomination\":20,\"quantity\":200},{\"denomination\":10,\"quantity\":50},{\"denomination\":5,\"quantity\":80},{\"denomination\":1,\"quantity\":100},{\"denomination\":0.5,\"quantity\":0},{\"denomination\":0.2,\"quantity\":0},{\"denomination\":0.1,\"quantity\":0},{\"denomination\":0.05,\"quantity\":0},{\"denomination\":0.01,\"quantity\":0}]}"; mockMvc.perform(post("/api/v1/commercial-banks/issuance/denomination-plan/validate") - .with(jwt().jwt(jwt -> jwt.subject("487").claim("preferred_username", "tzs001"))) + .with(jwt().jwt(jwt -> jwt.subject("487").claim("userId", 487L).claim("preferred_username", "tzs001"))) .contentType(MediaType.APPLICATION_JSON).content(requestBody)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.totalQuantity").value(930)) @@ -94,13 +94,22 @@ class IssuanceControllerTest { String requestBody = "{\"bankCode\":\"BKCHCNBJ00001\",\"organizationId\":\"ORG_3A4B5C6D7E8F\",\"totalAmount\":50000.00,\"currency\":\"DC\",\"denominations\":[{\"denomination\":100,\"quantity\":400},{\"denomination\":50,\"quantity\":100},{\"denomination\":20,\"quantity\":200},{\"denomination\":10,\"quantity\":50},{\"denomination\":5,\"quantity\":80},{\"denomination\":1,\"quantity\":100},{\"denomination\":0.5,\"quantity\":0},{\"denomination\":0.2,\"quantity\":0},{\"denomination\":0.1,\"quantity\":0},{\"denomination\":0.05,\"quantity\":0},{\"denomination\":0.01,\"quantity\":0}]}"; mockMvc.perform(post("/api/v1/commercial-banks/issuance/requests") - .with(jwt().jwt(jwt -> jwt.subject("487").claim("preferred_username", "tzs001"))) + .with(jwt().jwt(jwt -> jwt.subject("487").claim("userId", 487L).claim("preferred_username", "tzs001"))) .contentType(MediaType.APPLICATION_JSON).content(requestBody)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.status").value("DRAFT")) .andExpect(jsonPath("$.data.denominations.length()").value(6)); } + @Test + void rejectsAnIssuanceOperationWhenAuditClaimsAreMissing() throws Exception { + String requestBody = "{\"bankCode\":\"BKCHCNBJ00001\",\"organizationId\":\"ORG_3A4B5C6D7E8F\",\"totalAmount\":50000.00,\"currency\":\"DC\",\"denominations\":[{\"denomination\":100,\"quantity\":500}]}"; + mockMvc.perform(post("/api/v1/commercial-banks/issuance/requests") + .with(jwt().jwt(jwt -> jwt.subject("487").claim("preferred_username", "tzs001"))) + .contentType(MediaType.APPLICATION_JSON).content(requestBody)) + .andExpect(jsonPath("$.code").value("UNAUTHORIZED")); + } + @Test void swaggerDocumentsBothChineseIssuanceEnds() throws Exception { String apiDocs = new String(mockMvc.perform(get("/v3/api-docs"))