fix(p1r): 高敏 Account 导出缺少 step-up 时失败关闭

This commit is contained in:
zizi 2026-05-30 00:59:08 +08:00
parent 7aedfb9d08
commit 2ef35aedb3
2 changed files with 21 additions and 18 deletions

View File

@ -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);
}

View File

@ -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,