Browse Source
- rename RoleDetailVO -> RoleDTO extending BaseDTO, add toEntity() - new RoleParam extends BaseParam; page() now uses PageConverter (size clamp + orderBy anti-injection), fixing pagination bypass - saveOrUpdate 6 @RequestParam -> RoleDTO; detail returns RoleDTO - add PageConverterTest covering size clamp + orderBy injection guardmaster
9 changed files with 204 additions and 112 deletions
@ -0,0 +1,69 @@ |
|||
package com.crm.auth.domain.dto; |
|||
|
|||
import com.crm.auth.domain.entity.SysRole; |
|||
import com.crm.base.domain.dto.BaseDTO; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 角色传输对象(ADR-0012 / ADR-0017 Route A:saveOrUpdate 写入参 + detail 出参双向) |
|||
* <p>继承 {@link BaseDTO} 白拿 id/createTime/updateTime;createTime/updateTime 由全局 |
|||
* @InitBinder 在入参侧 strip,出参侧展示原值。builtin 亦由 @InitBinder strip(内置角色标记服务端裁定)。</p> |
|||
* <p>resourceIds 为已补全祖先的完整授权集合(ADR-0005),仅 detail 出参填充。</p> |
|||
*/ |
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
public class RoleDTO extends BaseDTO { |
|||
|
|||
@Schema(description = "角色名称") |
|||
private String roleName; |
|||
|
|||
@Schema(description = "角色编码") |
|||
private String roleCode; |
|||
|
|||
@Schema(description = "数据范围 1=本人 2=本部门 3=本部门及子部门 4=全部") |
|||
private Integer dataScope; |
|||
|
|||
@Schema(description = "排序") |
|||
private Integer sort; |
|||
|
|||
@Schema(description = "备注") |
|||
private String remark; |
|||
|
|||
@Schema(description = "是否内置角色") |
|||
private Boolean builtin; |
|||
|
|||
@Schema(description = "已补全祖先的完整授权集合(存储态)") |
|||
private List<Long> resourceIds; |
|||
|
|||
/** SysRole → RoleDTO 基本字段映射(不含 resourceIds) */ |
|||
public static RoleDTO fromEntity(SysRole entity) { |
|||
if (entity == null) { |
|||
return null; |
|||
} |
|||
RoleDTO dto = new RoleDTO(); |
|||
dto.setId(entity.getId()); |
|||
dto.setRoleName(entity.getRoleName()); |
|||
dto.setRoleCode(entity.getRoleCode()); |
|||
dto.setDataScope(entity.getDataScope()); |
|||
dto.setSort(entity.getSort()); |
|||
dto.setRemark(entity.getRemark()); |
|||
dto.setBuiltin(entity.getBuiltin()); |
|||
return dto; |
|||
} |
|||
|
|||
/** RoleDTO → SysRole(写入参转换,Service 层调用;builtin/审计字段不映射,由服务端裁定) */ |
|||
public SysRole toEntity() { |
|||
SysRole entity = new SysRole(); |
|||
entity.setId(this.getId()); |
|||
entity.setRoleName(this.roleName); |
|||
entity.setRoleCode(this.roleCode); |
|||
entity.setDataScope(this.dataScope); |
|||
entity.setSort(this.sort); |
|||
entity.setRemark(this.remark); |
|||
return entity; |
|||
} |
|||
} |
|||
@ -1,60 +0,0 @@ |
|||
package com.crm.auth.domain.dto; |
|||
|
|||
import com.crm.auth.domain.entity.SysRole; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
import java.io.Serial; |
|||
import java.io.Serializable; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 角色详情 DTO(ADR-0012) |
|||
* <p>角色管理详情接口的输出载体;resourceIds 为已补全祖先的完整授权集合(ADR-0005)</p> |
|||
*/ |
|||
@Data |
|||
public class RoleDetailVO implements Serializable { |
|||
|
|||
@Serial |
|||
private static final long serialVersionUID = 1L; |
|||
|
|||
@Schema(description = "角色 ID") |
|||
private Long id; |
|||
|
|||
@Schema(description = "角色名称") |
|||
private String roleName; |
|||
|
|||
@Schema(description = "角色编码") |
|||
private String roleCode; |
|||
|
|||
@Schema(description = "数据范围 1=本人 2=本部门 3=本部门及子部门 4=全部") |
|||
private Integer dataScope; |
|||
|
|||
@Schema(description = "排序") |
|||
private Integer sort; |
|||
|
|||
@Schema(description = "备注") |
|||
private String remark; |
|||
|
|||
@Schema(description = "是否内置角色") |
|||
private Boolean builtin; |
|||
|
|||
@Schema(description = "已补全祖先的完整授权集合(存储态)") |
|||
private List<Long> resourceIds; |
|||
|
|||
/** SysRole → RoleDetailVO 基本字段映射(不含 resourceIds) */ |
|||
public static RoleDetailVO fromEntity(SysRole entity) { |
|||
if (entity == null) { |
|||
return null; |
|||
} |
|||
RoleDetailVO vo = new RoleDetailVO(); |
|||
vo.setId(entity.getId()); |
|||
vo.setRoleName(entity.getRoleName()); |
|||
vo.setRoleCode(entity.getRoleCode()); |
|||
vo.setDataScope(entity.getDataScope()); |
|||
vo.setSort(entity.getSort()); |
|||
vo.setRemark(entity.getRemark()); |
|||
vo.setBuiltin(entity.getBuiltin()); |
|||
return vo; |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
package com.crm.auth.domain.param; |
|||
|
|||
import com.crm.base.domain.param.BaseParam; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
|
|||
/** |
|||
* 角色管理-分页查询入参(ADR-0017) |
|||
* <p>keyword 匹配角色名称,current/size/orderBy/asc 继承自 {@link BaseParam}, |
|||
* 经 {@link com.crm.base.utils.PageConverter#toMpPage} 转换(size 上限收敛 + 排序字段防注入)。</p> |
|||
*/ |
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
public class RoleParam extends BaseParam { |
|||
} |
|||
@ -0,0 +1,65 @@ |
|||
package com.crm.base.utils; |
|||
|
|||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
|||
import com.crm.base.constant.CommonConstants; |
|||
import com.crm.base.domain.param.BaseParam; |
|||
import org.junit.jupiter.api.DisplayName; |
|||
import org.junit.jupiter.api.Test; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
|
|||
/** |
|||
* {@link PageConverter#toMpPage} 单元测试:分页安全收敛(size 上限、排序字段防注入)。 |
|||
* <p>这是所有分页接口(含角色 page,ADR-0017 ticket 06)的共享护栏。</p> |
|||
*/ |
|||
@DisplayName("PageConverter 分页参数收敛") |
|||
class PageConverterTest { |
|||
|
|||
private static BaseParam param(Integer current, Integer size, String orderBy, Boolean asc) { |
|||
BaseParam p = new BaseParam(); |
|||
p.setCurrent(current); |
|||
p.setSize(size); |
|||
p.setOrderBy(orderBy); |
|||
p.setAsc(asc); |
|||
return p; |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("size 超过上限 500 被收敛为 500") |
|||
void size_exceedsMax_clampedToMax() { |
|||
Page<Object> page = PageConverter.toMpPage(param(1, 100_000, null, null)); |
|||
assertThat(page.getSize()).isEqualTo(CommonConstants.MAX_PAGE_SIZE); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("size 为 null 或 <1 回落默认值 10") |
|||
void size_nullOrNonPositive_fallsBackToDefault() { |
|||
assertThat(PageConverter.toMpPage(param(1, null, null, null)).getSize()) |
|||
.isEqualTo(CommonConstants.DEFAULT_PAGE_SIZE); |
|||
assertThat(PageConverter.toMpPage(param(1, 0, null, null)).getSize()) |
|||
.isEqualTo(CommonConstants.DEFAULT_PAGE_SIZE); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("current <1 回落到 1") |
|||
void current_nonPositive_fallsBackToOne() { |
|||
assertThat(PageConverter.toMpPage(param(0, 10, null, null)).getCurrent()).isEqualTo(1); |
|||
assertThat(PageConverter.toMpPage(param(null, 10, null, null)).getCurrent()).isEqualTo(1); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("合法 orderBy 驼峰转下划线并生成排序项") |
|||
void orderBy_validCamel_convertedToUnderlineOrder() { |
|||
Page<Object> page = PageConverter.toMpPage(param(1, 10, "createTime", true)); |
|||
assertThat(page.orders()).hasSize(1); |
|||
assertThat(page.orders().get(0).getColumn()).isEqualTo("create_time"); |
|||
assertThat(page.orders().get(0).isAsc()).isTrue(); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("非法 orderBy(含注入字符)被忽略,不产生排序项") |
|||
void orderBy_injectionChars_ignored() { |
|||
Page<Object> page = PageConverter.toMpPage(param(1, 10, "id; DROP TABLE sys_role", null)); |
|||
assertThat(page.orders()).isEmpty(); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue