fix(p1r): 高敏 Account 导出缺少 step-up 时失败关闭
This commit is contained in:
parent
7aedfb9d08
commit
2ef35aedb3
@ -53,7 +53,6 @@ public class AccountExportServiceImpl implements AccountExportService {
|
||||
private static final String STATUS_COMPLETED = "completed";
|
||||
private static final String STATUS_FAILED = "failed";
|
||||
private static final String STATUS_IDEMPOTENT_HIT = "idempotent_hit";
|
||||
private static final int SAFE_DATE_RANGE_DAYS = 31;
|
||||
private static final Set<String> EXPORT_TYPES = Set.of("profile", "usage", "security_events", "purchases", "licenses");
|
||||
private static final Set<String> DATE_RANGE_REQUIRED_TYPES = Set.of("usage", "security_events");
|
||||
private static final Set<String> SENSITIVE_EXPORT_TYPES = Set.of("security_events", "usage");
|
||||
@ -212,10 +211,9 @@ public class AccountExportServiceImpl implements AccountExportService {
|
||||
if (startDate != null && endDate != null && startDate.isAfter(endDate)) {
|
||||
throw invalidParamException("dateRange.startDate 不能晚于 endDate");
|
||||
}
|
||||
long rangeDays = startDate == null || endDate == null ? 0 : endDate.toEpochDay() - startDate.toEpochDay() + 1;
|
||||
if (sensitiveExportType(exportType) && rangeDays > SAFE_DATE_RANGE_DAYS) {
|
||||
// OpenAPI 当前没有 step-up 证明字段,大范围高敏导出必须 fail-closed,避免静默放开敏感数据导出。
|
||||
throw invalidParamException("高敏导出超过 {} 天需要 step-up,当前请求缺少 step-up 证明", SAFE_DATE_RANGE_DAYS);
|
||||
if (sensitiveExportType(exportType)) {
|
||||
// OpenAPI 只声明高敏导出需要 step-up,但当前请求体没有可信 step-up proof 字段;服务端必须失败关闭。
|
||||
throw invalidParamException("{} 高敏导出当前缺少 step-up 证明,暂不可执行", exportType);
|
||||
}
|
||||
return new ExportRequest(exportType, startDate, endDate, desensitizationRule);
|
||||
}
|
||||
|
||||
@ -9,7 +9,6 @@ import cn.iocoder.muse.module.member.controller.app.account.vo.AccountExportTask
|
||||
import cn.iocoder.muse.module.member.dal.dataobject.account.AccountCommandDO;
|
||||
import cn.iocoder.muse.module.member.dal.dataobject.account.AccountDownloadCredentialDO;
|
||||
import cn.iocoder.muse.module.member.dal.dataobject.account.AccountExportTaskDO;
|
||||
import cn.iocoder.muse.module.member.dal.dataobject.account.MemberSecurityEventDO;
|
||||
import cn.iocoder.muse.module.member.dal.dataobject.user.MemberUserDO;
|
||||
import cn.iocoder.muse.module.member.dal.mysql.account.AccountDownloadCredentialMapper;
|
||||
import cn.iocoder.muse.module.member.dal.mysql.account.AccountExportTaskMapper;
|
||||
@ -57,8 +56,8 @@ class AccountExportServiceTest extends BaseMockitoUnitTest {
|
||||
private AccountFileServiceFacade fileServiceFacade;
|
||||
|
||||
@Test
|
||||
void should_createQueuedSecurityExport_andWriteSensitiveSecurityEvent_whenFileServiceUnavailable() {
|
||||
AccountExportTaskCreateReqVO reqVO = exportReq("security_events", "cmd-export-1");
|
||||
void should_createQueuedProfileExport_whenFileServiceUnavailable() {
|
||||
AccountExportTaskCreateReqVO reqVO = exportReq("profile", "cmd-export-1");
|
||||
when(memberUserMapper.selectById(1001L)).thenReturn(user(1001L));
|
||||
when(commandService.buildRequestHash(any())).thenReturn("hash-export");
|
||||
when(commandService.reserveCommand(any())).thenReturn(null);
|
||||
@ -77,17 +76,12 @@ class AccountExportServiceTest extends BaseMockitoUnitTest {
|
||||
assertNotEquals("pending", result.getStatus());
|
||||
verify(exportTaskMapper).insert(argThat((AccountExportTaskDO task) ->
|
||||
Long.valueOf(1001L).equals(task.getAccountUserId())
|
||||
&& "security_events".equals(task.getExportType())
|
||||
&& "profile".equals(task.getExportType())
|
||||
&& "queued".equals(task.getStatus())
|
||||
&& task.getFileRef() == null
|
||||
&& Boolean.FALSE.equals(task.getSourceBlocked())));
|
||||
verify(downloadCredentialMapper, never()).insert(any(AccountDownloadCredentialDO.class));
|
||||
verify(securityEventMapper).insert(argThat((MemberSecurityEventDO event) ->
|
||||
Long.valueOf(1001L).equals(event.getAccountUserId())
|
||||
&& "sensitive_export".equals(event.getEventType())
|
||||
&& "warning".equals(event.getSeverity())
|
||||
&& event.getDeviceInfo() != null
|
||||
&& !event.getDeviceInfo().contains("credential")));
|
||||
verifyNoInteractions(securityEventMapper);
|
||||
verify(commandService).recordSucceeded(any(), argThat(snapshot -> snapshot.contains("\"status\":\"queued\"")));
|
||||
verify(auditService).record(argThat(req ->
|
||||
"appCreateExportTask".equals(req.getOperationId())
|
||||
@ -114,10 +108,21 @@ class AccountExportServiceTest extends BaseMockitoUnitTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_failClosed_whenSecurityExportRangeRequiresStepUpButNoStepUpFieldExists() {
|
||||
void should_failClosed_whenSecurityEventsExportLacksStepUpProof() {
|
||||
AccountExportTaskCreateReqVO reqVO = exportReq("security_events", "cmd-export-1");
|
||||
reqVO.getDateRange().setStartDate(LocalDate.of(2026, 1, 1));
|
||||
reqVO.getDateRange().setEndDate(LocalDate.of(2026, 5, 28));
|
||||
when(memberUserMapper.selectById(1001L)).thenReturn(user(1001L));
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> exportService.appCreateExportTask(1001L, reqVO));
|
||||
|
||||
assertEquals(400, exception.getCode());
|
||||
assertTrue(exception.getMessage().contains("step-up"));
|
||||
verifyNoInteractions(commandService, fileServiceFacade, exportTaskMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_failClosed_whenUsageExportLacksStepUpProof() {
|
||||
AccountExportTaskCreateReqVO reqVO = exportReq("usage", "cmd-export-1");
|
||||
when(memberUserMapper.selectById(1001L)).thenReturn(user(1001L));
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user