mirror of
https://gitee.com/hhyykk/ipms-sjy.git
synced 2025-09-08 22:21:54 +08:00
yudao-spring-boot-starter-data-permission 和 yudao-spring-boot-starter-tenant 调整为 biz 组件
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>yudao-framework</artifactId>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>yudao-spring-boot-starter-biz-data-permission</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${artifactId}</name>
|
||||
<description>数据权限</description>
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-security</artifactId>
|
||||
<optional>true</optional> <!-- 可选,如果使用 DeptDataPermissionRule 必须提供 -->
|
||||
</dependency>
|
||||
|
||||
<!-- DB 相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-mybatis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test 测试相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@@ -0,0 +1,45 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.db.DataPermissionDatabaseInterceptor;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 数据全新啊的自动配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration
|
||||
public class YudaoDataPermissionAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public DataPermissionRuleFactory dataPermissionRuleFactory(List<DataPermissionRule> rules) {
|
||||
return new DataPermissionRuleFactoryImpl(rules);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DataPermissionDatabaseInterceptor dataPermissionDatabaseInterceptor(MybatisPlusInterceptor interceptor,
|
||||
List<DataPermissionRule> rules) {
|
||||
// 创建 DataPermissionDatabaseInterceptor 拦截器
|
||||
DataPermissionRuleFactory ruleFactory = dataPermissionRuleFactory(rules);
|
||||
DataPermissionDatabaseInterceptor inner = new DataPermissionDatabaseInterceptor(ruleFactory);
|
||||
// 添加到 interceptor 中
|
||||
// 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
|
||||
MyBatisUtils.addInterceptor(interceptor, inner, 0);
|
||||
return inner;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() {
|
||||
return new DataPermissionAnnotationAdvisor();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.datapermission.core.dept.rule.DeptDataPermissionRule;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.dept.rule.DeptDataPermissionRuleCustomizer;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.dept.service.DeptDataPermissionFrameworkService;
|
||||
import cn.iocoder.yudao.framework.security.core.LoginUser;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 基于部门的数据权限 AutoConfiguration
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnClass(LoginUser.class)
|
||||
@ConditionalOnBean(value = {DeptDataPermissionFrameworkService.class, DeptDataPermissionRuleCustomizer.class})
|
||||
public class YudaoDeptDataPermissionAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public DeptDataPermissionRule deptDataPermissionRule(DeptDataPermissionFrameworkService service,
|
||||
List<DeptDataPermissionRuleCustomizer> customizers) {
|
||||
// 创建 DeptDataPermissionRule 对象
|
||||
DeptDataPermissionRule rule = new DeptDataPermissionRule(service);
|
||||
// 补全表配置
|
||||
customizers.forEach(customizer -> customizer.customize(rule));
|
||||
return rule;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.annotation;
|
||||
|
||||
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 数据权限注解
|
||||
* 可声明在类或者方法上,标识使用的数据权限规则
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface DataPermission {
|
||||
|
||||
/**
|
||||
* 当前类或方法是否开启数据权限
|
||||
* 即使不添加 @DataPermission 注解,默认是开启状态
|
||||
* 可通过设置 enable 为 false 禁用
|
||||
*/
|
||||
boolean enable() default true;
|
||||
|
||||
/**
|
||||
* 生效的数据权限规则数组,优先级高于 {@link #excludeRules()}
|
||||
*/
|
||||
Class<? extends DataPermissionRule>[] includeRules() default {};
|
||||
|
||||
/**
|
||||
* 排除的数据权限规则数组,优先级最低
|
||||
*/
|
||||
Class<? extends DataPermissionRule>[] excludeRules() default {};
|
||||
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.aop;
|
||||
|
||||
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import org.aopalliance.aop.Advice;
|
||||
import org.springframework.aop.Pointcut;
|
||||
import org.springframework.aop.support.AbstractPointcutAdvisor;
|
||||
import org.springframework.aop.support.ComposablePointcut;
|
||||
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
|
||||
|
||||
/**
|
||||
* {@link cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission} 注解的 Advisor 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Getter
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor {
|
||||
|
||||
private final Advice advice;
|
||||
|
||||
private final Pointcut pointcut;
|
||||
|
||||
public DataPermissionAnnotationAdvisor() {
|
||||
this.advice = new DataPermissionAnnotationInterceptor();
|
||||
this.pointcut = this.buildPointcut();
|
||||
}
|
||||
|
||||
protected Pointcut buildPointcut() {
|
||||
Pointcut classPointcut = new AnnotationMatchingPointcut(DataPermission.class, true);
|
||||
Pointcut methodPointcut = new AnnotationMatchingPointcut(null, DataPermission.class, true);
|
||||
return new ComposablePointcut(classPointcut).union(methodPointcut);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,72 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.aop;
|
||||
|
||||
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
|
||||
import lombok.Getter;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.springframework.core.MethodClassKey;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* {@link DataPermission} 注解的拦截器
|
||||
* 1. 在执行方法前,将 @DataPermission 注解入栈
|
||||
* 2. 在执行方法后,将 @DataPermission 注解出栈
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@DataPermission // 该注解,用于 {@link DATA_PERMISSION_NULL} 的空对象
|
||||
public class DataPermissionAnnotationInterceptor implements MethodInterceptor {
|
||||
|
||||
/**
|
||||
* DataPermission 空对象,用于方法无 {@link DataPermission} 注解时,使用 DATA_PERMISSION_NULL 进行占位
|
||||
*/
|
||||
static final DataPermission DATA_PERMISSION_NULL = DataPermissionAnnotationInterceptor.class.getAnnotation(DataPermission.class);
|
||||
|
||||
@Getter
|
||||
private final Map<MethodClassKey, DataPermission> dataPermissionCache = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
|
||||
// 入栈
|
||||
DataPermission dataPermission = this.findAnnotation(methodInvocation);
|
||||
if (dataPermission != null) {
|
||||
DataPermissionContextHolder.add(dataPermission);
|
||||
}
|
||||
try {
|
||||
// 执行逻辑
|
||||
return methodInvocation.proceed();
|
||||
} finally {
|
||||
// 出栈
|
||||
if (dataPermission != null) {
|
||||
DataPermissionContextHolder.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DataPermission findAnnotation(MethodInvocation methodInvocation) {
|
||||
// 1. 从缓存中获取
|
||||
Method method = methodInvocation.getMethod();
|
||||
Object targetObject = methodInvocation.getThis();
|
||||
Class<?> clazz = targetObject != null ? targetObject.getClass() : method.getDeclaringClass();
|
||||
MethodClassKey methodClassKey = new MethodClassKey(method, clazz);
|
||||
DataPermission dataPermission = dataPermissionCache.get(methodClassKey);
|
||||
if (dataPermission != null) {
|
||||
return dataPermission != DATA_PERMISSION_NULL ? dataPermission : null;
|
||||
}
|
||||
|
||||
// 2.1 从方法中获取
|
||||
dataPermission = AnnotationUtils.findAnnotation(method, DataPermission.class);
|
||||
// 2.2 从类上获取
|
||||
if (dataPermission == null) {
|
||||
dataPermission = AnnotationUtils.findAnnotation(clazz, DataPermission.class);
|
||||
}
|
||||
// 2.3 添加到缓存中
|
||||
dataPermissionCache.put(methodClassKey, dataPermission != null ? dataPermission : DATA_PERMISSION_NULL);
|
||||
return dataPermission;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,72 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.aop;
|
||||
|
||||
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
|
||||
import com.alibaba.ttl.TransmittableThreadLocal;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link DataPermission} 注解的 Context 上下文
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class DataPermissionContextHolder {
|
||||
|
||||
/**
|
||||
* 使用 List 的原因,可能存在方法的嵌套调用
|
||||
*/
|
||||
private static final ThreadLocal<LinkedList<DataPermission>> DATA_PERMISSIONS =
|
||||
TransmittableThreadLocal.withInitial(LinkedList::new);
|
||||
|
||||
/**
|
||||
* 获得当前的 DataPermission 注解
|
||||
*
|
||||
* @return DataPermission 注解
|
||||
*/
|
||||
public static DataPermission get() {
|
||||
return DATA_PERMISSIONS.get().peekLast();
|
||||
}
|
||||
|
||||
/**
|
||||
* 入栈 DataPermission 注解
|
||||
*
|
||||
* @param dataPermission DataPermission 注解
|
||||
*/
|
||||
public static void add(DataPermission dataPermission) {
|
||||
DATA_PERMISSIONS.get().addLast(dataPermission);
|
||||
}
|
||||
|
||||
/**
|
||||
* 出栈 DataPermission 注解
|
||||
*
|
||||
* @return DataPermission 注解
|
||||
*/
|
||||
public static DataPermission remove() {
|
||||
DataPermission dataPermission = DATA_PERMISSIONS.get().removeLast();
|
||||
// 无元素时,清空 ThreadLocal
|
||||
if (DATA_PERMISSIONS.get().isEmpty()) {
|
||||
DATA_PERMISSIONS.remove();
|
||||
}
|
||||
return dataPermission;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得所有 DataPermission
|
||||
*
|
||||
* @return DataPermission 队列
|
||||
*/
|
||||
public static List<DataPermission> getAll() {
|
||||
return DATA_PERMISSIONS.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空上下文
|
||||
*
|
||||
* 目前仅仅用于单测
|
||||
*/
|
||||
public static void clear() {
|
||||
DATA_PERMISSIONS.remove();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,507 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.db;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
|
||||
import com.alibaba.ttl.TransmittableThreadLocal;
|
||||
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
|
||||
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
|
||||
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.sf.jsqlparser.expression.*;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.ExistsExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
|
||||
import net.sf.jsqlparser.expression.operators.relational.InExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.ItemsList;
|
||||
import net.sf.jsqlparser.schema.Table;
|
||||
import net.sf.jsqlparser.statement.delete.Delete;
|
||||
import net.sf.jsqlparser.statement.select.*;
|
||||
import net.sf.jsqlparser.statement.update.Update;
|
||||
import org.apache.ibatis.executor.Executor;
|
||||
import org.apache.ibatis.executor.statement.StatementHandler;
|
||||
import org.apache.ibatis.mapping.BoundSql;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.apache.ibatis.mapping.SqlCommandType;
|
||||
import org.apache.ibatis.session.ResultHandler;
|
||||
import org.apache.ibatis.session.RowBounds;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 数据权限拦截器,通过 {@link DataPermissionRule} 数据权限规则,重写 SQL 的方式来实现
|
||||
* 主要的 SQL 重写方法,可见 {@link #builderExpression(Expression, Table)} 方法
|
||||
*
|
||||
* 整体的代码实现上,参考 {@link com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor} 实现。
|
||||
* 所以每次 MyBatis Plus 升级时,需要 Review 下其具体的实现是否有变更!
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class DataPermissionDatabaseInterceptor extends JsqlParserSupport implements InnerInterceptor {
|
||||
|
||||
private final DataPermissionRuleFactory ruleFactory;
|
||||
|
||||
@Getter
|
||||
private final MappedStatementCache mappedStatementCache = new MappedStatementCache();
|
||||
|
||||
@Override // SELECT 场景
|
||||
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter,
|
||||
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
|
||||
// 获得 Mapper 对应的数据权限的规则
|
||||
List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(ms.getId());
|
||||
if (mappedStatementCache.noRewritable(ms, rules)) { // 如果无需重写,则跳过
|
||||
return;
|
||||
}
|
||||
|
||||
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
|
||||
try {
|
||||
// 初始化上下文
|
||||
ContextHolder.init(rules);
|
||||
// 处理 SQL
|
||||
mpBs.sql(parserSingle(mpBs.sql(), null));
|
||||
} finally {
|
||||
addMappedStatementCache(ms);
|
||||
ContextHolder.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override // 只处理 UPDATE / DELETE 场景,不处理 INSERT 场景
|
||||
public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
|
||||
PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
|
||||
MappedStatement ms = mpSh.mappedStatement();
|
||||
SqlCommandType sct = ms.getSqlCommandType();
|
||||
if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
|
||||
// 获得 Mapper 对应的数据权限的规则
|
||||
List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(ms.getId());
|
||||
if (mappedStatementCache.noRewritable(ms, rules)) { // 如果无需重写,则跳过
|
||||
return;
|
||||
}
|
||||
|
||||
PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
|
||||
try {
|
||||
// 初始化上下文
|
||||
ContextHolder.init(rules);
|
||||
// 处理 SQL
|
||||
mpBs.sql(parserMulti(mpBs.sql(), null));
|
||||
} finally {
|
||||
addMappedStatementCache(ms);
|
||||
ContextHolder.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processSelect(Select select, int index, String sql, Object obj) {
|
||||
processSelectBody(select.getSelectBody());
|
||||
List<WithItem> withItemsList = select.getWithItemsList();
|
||||
if (!CollectionUtils.isEmpty(withItemsList)) {
|
||||
withItemsList.forEach(this::processSelectBody);
|
||||
}
|
||||
}
|
||||
|
||||
protected void processSelectBody(SelectBody selectBody) {
|
||||
if (selectBody == null) {
|
||||
return;
|
||||
}
|
||||
if (selectBody instanceof PlainSelect) {
|
||||
processPlainSelect((PlainSelect) selectBody);
|
||||
} else if (selectBody instanceof WithItem) {
|
||||
WithItem withItem = (WithItem) selectBody;
|
||||
processSelectBody(withItem.getSubSelect().getSelectBody());
|
||||
} else {
|
||||
SetOperationList operationList = (SetOperationList) selectBody;
|
||||
List<SelectBody> selectBodys = operationList.getSelects();
|
||||
if (CollectionUtils.isNotEmpty(selectBodys)) {
|
||||
selectBodys.forEach(this::processSelectBody);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* update 语句处理
|
||||
*/
|
||||
@Override
|
||||
protected void processUpdate(Update update, int index, String sql, Object obj) {
|
||||
final Table table = update.getTable();
|
||||
update.setWhere(this.builderExpression(update.getWhere(), table));
|
||||
}
|
||||
|
||||
/**
|
||||
* delete 语句处理
|
||||
*/
|
||||
@Override
|
||||
protected void processDelete(Delete delete, int index, String sql, Object obj) {
|
||||
delete.setWhere(this.builderExpression(delete.getWhere(), delete.getTable()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 PlainSelect
|
||||
*/
|
||||
protected void processPlainSelect(PlainSelect plainSelect) {
|
||||
FromItem fromItem = plainSelect.getFromItem();
|
||||
Expression where = plainSelect.getWhere();
|
||||
processWhereSubSelect(where);
|
||||
if (fromItem instanceof Table) {
|
||||
Table fromTable = (Table) fromItem;
|
||||
plainSelect.setWhere(builderExpression(where, fromTable));
|
||||
} else {
|
||||
processFromItem(fromItem);
|
||||
}
|
||||
//#3087 github
|
||||
List<SelectItem> selectItems = plainSelect.getSelectItems();
|
||||
if (CollectionUtils.isNotEmpty(selectItems)) {
|
||||
selectItems.forEach(this::processSelectItem);
|
||||
}
|
||||
List<Join> joins = plainSelect.getJoins();
|
||||
if (CollectionUtils.isNotEmpty(joins)) {
|
||||
processJoins(joins);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理where条件内的子查询
|
||||
* <p>
|
||||
* 支持如下:
|
||||
* 1. in
|
||||
* 2. =
|
||||
* 3. >
|
||||
* 4. <
|
||||
* 5. >=
|
||||
* 6. <=
|
||||
* 7. <>
|
||||
* 8. EXISTS
|
||||
* 9. NOT EXISTS
|
||||
* <p>
|
||||
* 前提条件:
|
||||
* 1. 子查询必须放在小括号中
|
||||
* 2. 子查询一般放在比较操作符的右边
|
||||
*
|
||||
* @param where where 条件
|
||||
*/
|
||||
protected void processWhereSubSelect(Expression where) {
|
||||
if (where == null) {
|
||||
return;
|
||||
}
|
||||
if (where instanceof FromItem) {
|
||||
processFromItem((FromItem) where);
|
||||
return;
|
||||
}
|
||||
if (where.toString().indexOf("SELECT") > 0) {
|
||||
// 有子查询
|
||||
if (where instanceof BinaryExpression) {
|
||||
// 比较符号 , and , or , 等等
|
||||
BinaryExpression expression = (BinaryExpression) where;
|
||||
processWhereSubSelect(expression.getLeftExpression());
|
||||
processWhereSubSelect(expression.getRightExpression());
|
||||
} else if (where instanceof InExpression) {
|
||||
// in
|
||||
InExpression expression = (InExpression) where;
|
||||
ItemsList itemsList = expression.getRightItemsList();
|
||||
if (itemsList instanceof SubSelect) {
|
||||
processSelectBody(((SubSelect) itemsList).getSelectBody());
|
||||
}
|
||||
} else if (where instanceof ExistsExpression) {
|
||||
// exists
|
||||
ExistsExpression expression = (ExistsExpression) where;
|
||||
processWhereSubSelect(expression.getRightExpression());
|
||||
} else if (where instanceof NotExpression) {
|
||||
// not exists
|
||||
NotExpression expression = (NotExpression) where;
|
||||
processWhereSubSelect(expression.getExpression());
|
||||
} else if (where instanceof Parenthesis) {
|
||||
Parenthesis expression = (Parenthesis) where;
|
||||
processWhereSubSelect(expression.getExpression());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void processSelectItem(SelectItem selectItem) {
|
||||
if (selectItem instanceof SelectExpressionItem) {
|
||||
SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
|
||||
if (selectExpressionItem.getExpression() instanceof SubSelect) {
|
||||
processSelectBody(((SubSelect) selectExpressionItem.getExpression()).getSelectBody());
|
||||
} else if (selectExpressionItem.getExpression() instanceof Function) {
|
||||
processFunction((Function) selectExpressionItem.getExpression());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理函数
|
||||
* <p>支持: 1. select fun(args..) 2. select fun1(fun2(args..),args..)<p>
|
||||
* <p> fixed gitee pulls/141</p>
|
||||
*
|
||||
* @param function 函数
|
||||
*/
|
||||
protected void processFunction(Function function) {
|
||||
ExpressionList parameters = function.getParameters();
|
||||
if (parameters != null) {
|
||||
parameters.getExpressions().forEach(expression -> {
|
||||
if (expression instanceof SubSelect) {
|
||||
processSelectBody(((SubSelect) expression).getSelectBody());
|
||||
} else if (expression instanceof Function) {
|
||||
processFunction((Function) expression);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理子查询等
|
||||
*/
|
||||
protected void processFromItem(FromItem fromItem) {
|
||||
if (fromItem instanceof SubJoin) {
|
||||
SubJoin subJoin = (SubJoin) fromItem;
|
||||
if (subJoin.getJoinList() != null) {
|
||||
processJoins(subJoin.getJoinList());
|
||||
}
|
||||
if (subJoin.getLeft() != null) {
|
||||
processFromItem(subJoin.getLeft());
|
||||
}
|
||||
} else if (fromItem instanceof SubSelect) {
|
||||
SubSelect subSelect = (SubSelect) fromItem;
|
||||
if (subSelect.getSelectBody() != null) {
|
||||
processSelectBody(subSelect.getSelectBody());
|
||||
}
|
||||
} else if (fromItem instanceof ValuesList) {
|
||||
logger.debug("Perform a subquery, if you do not give us feedback");
|
||||
} else if (fromItem instanceof LateralSubSelect) {
|
||||
LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem;
|
||||
if (lateralSubSelect.getSubSelect() != null) {
|
||||
SubSelect subSelect = lateralSubSelect.getSubSelect();
|
||||
if (subSelect.getSelectBody() != null) {
|
||||
processSelectBody(subSelect.getSelectBody());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 joins
|
||||
*
|
||||
* @param joins join 集合
|
||||
*/
|
||||
private void processJoins(List<Join> joins) {
|
||||
//对于 on 表达式写在最后的 join,需要记录下前面多个 on 的表名
|
||||
Deque<Table> tables = new LinkedList<>();
|
||||
for (Join join : joins) {
|
||||
// 处理 on 表达式
|
||||
FromItem fromItem = join.getRightItem();
|
||||
if (fromItem instanceof Table) {
|
||||
Table fromTable = (Table) fromItem;
|
||||
// 获取 join 尾缀的 on 表达式列表
|
||||
Collection<Expression> originOnExpressions = join.getOnExpressions();
|
||||
// 正常 join on 表达式只有一个,立刻处理
|
||||
if (originOnExpressions.size() == 1) {
|
||||
processJoin(join);
|
||||
continue;
|
||||
}
|
||||
tables.push(fromTable);
|
||||
// 尾缀多个 on 表达式的时候统一处理
|
||||
if (originOnExpressions.size() > 1) {
|
||||
Collection<Expression> onExpressions = new LinkedList<>();
|
||||
for (Expression originOnExpression : originOnExpressions) {
|
||||
Table currentTable = tables.poll();
|
||||
onExpressions.add(builderExpression(originOnExpression, currentTable));
|
||||
}
|
||||
join.setOnExpressions(onExpressions);
|
||||
}
|
||||
} else {
|
||||
// 处理右边连接的子表达式
|
||||
processFromItem(fromItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理联接语句
|
||||
*/
|
||||
protected void processJoin(Join join) {
|
||||
if (join.getRightItem() instanceof Table) {
|
||||
Table fromTable = (Table) join.getRightItem();
|
||||
Expression originOnExpression = CollUtil.getFirst(join.getOnExpressions());
|
||||
originOnExpression = builderExpression(originOnExpression, fromTable);
|
||||
join.setOnExpressions(CollUtil.newArrayList(originOnExpression));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理条件
|
||||
*/
|
||||
protected Expression builderExpression(Expression currentExpression, Table table) {
|
||||
// 获得 Table 对应的数据权限条件
|
||||
Expression equalsTo = buildDataPermissionExpression(table);
|
||||
if (equalsTo == null) { // 如果没条件,则返回 currentExpression 默认
|
||||
return currentExpression;
|
||||
}
|
||||
|
||||
// 表达式为空,则直接返回 equalsTo
|
||||
if (currentExpression == null) {
|
||||
return equalsTo;
|
||||
}
|
||||
// 如果表达式为 Or,则需要 (currentExpression) AND equalsTo
|
||||
if (currentExpression instanceof OrExpression) {
|
||||
return new AndExpression(new Parenthesis(currentExpression), equalsTo);
|
||||
}
|
||||
// 如果表达式为 And,则直接返回 currentExpression AND equalsTo
|
||||
return new AndExpression(currentExpression, equalsTo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建指定表的数据权限的 Expression 过滤条件
|
||||
*
|
||||
* @param table 表
|
||||
* @return Expression 过滤条件
|
||||
*/
|
||||
private Expression buildDataPermissionExpression(Table table) {
|
||||
// 生成条件
|
||||
Expression allExpression = null;
|
||||
for (DataPermissionRule rule : ContextHolder.getRules()) {
|
||||
// 判断表名是否匹配
|
||||
if (!rule.getTableNames().contains(table.getName())) {
|
||||
continue;
|
||||
}
|
||||
// 单条规则的条件
|
||||
String tableName = MyBatisUtils.getTableName(table);
|
||||
Expression oneExpress = rule.getExpression(tableName, table.getAlias());
|
||||
// 拼接到 allExpression 中
|
||||
allExpression = allExpression == null ? oneExpress
|
||||
: new AndExpression(allExpression, oneExpress);
|
||||
}
|
||||
|
||||
// 如果条件非空,说明已经重写了
|
||||
if (allExpression != null) {
|
||||
ContextHolder.setRewrite(true);
|
||||
}
|
||||
return allExpression;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 SQL 是否重写。如果没有重写,则添加到 {@link MappedStatementCache} 中
|
||||
*
|
||||
* @param ms MappedStatement
|
||||
*/
|
||||
private void addMappedStatementCache(MappedStatement ms) {
|
||||
if (ContextHolder.getRewrite()) {
|
||||
return;
|
||||
}
|
||||
// 有重写,进行添加
|
||||
mappedStatementCache.addNoRewritable(ms, ContextHolder.getRules());
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL 解析上下文,方便透传 {@link DataPermissionRule} 规则
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
static final class ContextHolder {
|
||||
|
||||
/**
|
||||
* 该 {@link MappedStatement} 对应的规则
|
||||
*/
|
||||
private static final ThreadLocal<List<DataPermissionRule>> RULES = new TransmittableThreadLocal<>();
|
||||
/**
|
||||
* SQL 是否进行重写
|
||||
*/
|
||||
private static final ThreadLocal<Boolean> REWRITE = new TransmittableThreadLocal<>();
|
||||
|
||||
public static void init(List<DataPermissionRule> rules) {
|
||||
RULES.set(rules);
|
||||
REWRITE.set(false);
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
RULES.remove();
|
||||
REWRITE.remove();
|
||||
}
|
||||
|
||||
public static boolean getRewrite() {
|
||||
return REWRITE.get();
|
||||
}
|
||||
|
||||
public static void setRewrite(boolean rewrite) {
|
||||
REWRITE.set(rewrite);
|
||||
}
|
||||
|
||||
public static List<DataPermissionRule> getRules() {
|
||||
return RULES.get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link MappedStatement} 缓存
|
||||
* 目前主要用于,记录 {@link DataPermissionRule} 是否对指定 {@link MappedStatement} 无效
|
||||
* 如果无效,则可以避免 SQL 的解析,加快速度
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
static final class MappedStatementCache {
|
||||
|
||||
/**
|
||||
* 指定数据权限规则,对指定 MappedStatement 无需重写(不生效)的缓存
|
||||
*
|
||||
* value:{@link MappedStatement#getId()} 编号
|
||||
*/
|
||||
@Getter
|
||||
private final Map<Class<? extends DataPermissionRule>, Set<String>> noRewritableMappedStatements = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 判断是否无需重写
|
||||
* ps:虽然有点中文式英语,但是容易读懂即可
|
||||
*
|
||||
* @param ms MappedStatement
|
||||
* @param rules 数据权限规则数组
|
||||
* @return 是否无需重写
|
||||
*/
|
||||
public boolean noRewritable(MappedStatement ms, List<DataPermissionRule> rules) {
|
||||
// 如果规则为空,说明无需重写
|
||||
if (CollUtil.isEmpty(rules)) {
|
||||
return true;
|
||||
}
|
||||
// 任一规则不在 noRewritableMap 中,则说明可能需要重写
|
||||
for (DataPermissionRule rule : rules) {
|
||||
Set<String> mappedStatementIds = noRewritableMappedStatements.get(rule.getClass());
|
||||
if (!CollUtil.contains(mappedStatementIds, ms.getId())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加无需重写的 MappedStatement
|
||||
*
|
||||
* @param ms MappedStatement
|
||||
* @param rules 数据权限规则数组
|
||||
*/
|
||||
public void addNoRewritable(MappedStatement ms, List<DataPermissionRule> rules) {
|
||||
for (DataPermissionRule rule : rules) {
|
||||
Set<String> mappedStatementIds = noRewritableMappedStatements.get(rule.getClass());
|
||||
if (CollUtil.isNotEmpty(mappedStatementIds)) {
|
||||
mappedStatementIds.add(ms.getId());
|
||||
} else {
|
||||
noRewritableMappedStatements.put(rule.getClass(), SetUtils.asSet(ms.getId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空缓存
|
||||
* 目前主要提供给单元测试
|
||||
*/
|
||||
public void clear() {
|
||||
noRewritableMappedStatements.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 基于部门的数据权限规则
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
package cn.iocoder.yudao.framework.datapermission.core.dept;
|
@@ -0,0 +1,186 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.dept.rule;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.dept.service.DeptDataPermissionFrameworkService;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO;
|
||||
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
|
||||
import cn.iocoder.yudao.framework.security.core.LoginUser;
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.expression.Alias;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.LongValue;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
|
||||
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
|
||||
import net.sf.jsqlparser.expression.operators.relational.InExpression;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 基于部门的 {@link DataPermissionRule} 数据权限规则实现
|
||||
*
|
||||
* 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。
|
||||
*
|
||||
* 实际业务场景下,会存在一个经典的问题?当用户修改部门时,冗余的 dept_id 是否需要修改?
|
||||
* 1. 一般情况下,dept_id 不进行修改,则会导致用户看到之前的数据。【yudao-admin-server 采用该方案】
|
||||
* 2. 部分情况下,希望该用户还是能看到之前的数据,则有两种方式解决:【需要你改造该 DeptDataPermissionRule 的实现代码】
|
||||
* 1)编写洗数据的脚本,将 dept_id 修改成新部门的编号;【建议】
|
||||
* 最终过滤条件是 WHERE dept_id = ?
|
||||
* 2)洗数据的话,可能涉及的数据量较大,也可以采用 user_id 进行过滤的方式,此时需要获取到 dept_id 对应的所有 user_id 用户编号;
|
||||
* 最终过滤条件是 WHERE user_id IN (?, ?, ? ...)
|
||||
* 3)想要保证原 dept_id 和 user_id 都可以看的到,此时使用 dept_id 和 user_id 一起过滤;
|
||||
* 最终过滤条件是 WHERE dept_id = ? OR user_id IN (?, ?, ? ...)
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
public class DeptDataPermissionRule implements DataPermissionRule {
|
||||
|
||||
private static final String DEPT_COLUMN_NAME = "dept_id";
|
||||
private static final String USER_COLUMN_NAME = "user_id";
|
||||
|
||||
private final DeptDataPermissionFrameworkService deptDataPermissionService;
|
||||
|
||||
/**
|
||||
* 基于部门的表字段配置
|
||||
* 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。
|
||||
*
|
||||
* key:表名
|
||||
* value:字段名
|
||||
*/
|
||||
private final Map<String, String> deptColumns = new HashMap<>();
|
||||
/**
|
||||
* 基于用户的表字段配置
|
||||
* 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。
|
||||
*
|
||||
* key:表名
|
||||
* value:字段名
|
||||
*/
|
||||
private final Map<String, String> userColumns = new HashMap<>();
|
||||
/**
|
||||
* 所有表名,是 {@link #deptColumns} 和 {@link #userColumns} 的合集
|
||||
*/
|
||||
private final Set<String> TABLE_NAMES = new HashSet<>();
|
||||
|
||||
@Override
|
||||
public Set<String> getTableNames() {
|
||||
return TABLE_NAMES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression getExpression(String tableName, Alias tableAlias) {
|
||||
// 只有有登陆用户的情况下,才进行数据权限的处理
|
||||
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
|
||||
if (loginUser == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获得数据权限
|
||||
DeptDataPermissionRespDTO deptDataPermission = deptDataPermissionService.getDeptDataPermission(loginUser);
|
||||
if (deptDataPermission == null) {
|
||||
log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser));
|
||||
throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限",
|
||||
loginUser.getId(), tableName, tableAlias.getName()));
|
||||
}
|
||||
|
||||
// 情况一,如果是 ALL 可查看全部,则无需拼接条件
|
||||
if (deptDataPermission.getAll()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限
|
||||
if (CollUtil.isEmpty(deptDataPermission.getDeptIds())
|
||||
&& Boolean.FALSE.equals(deptDataPermission.getSelf())) {
|
||||
return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空
|
||||
}
|
||||
|
||||
// 情况三,拼接 Dept 和 User 的条件,最后组合
|
||||
Expression deptExpression = this.buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds());
|
||||
Expression userExpression = this.buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId());
|
||||
if (deptExpression == null && userExpression == null) {
|
||||
log.error("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]",
|
||||
JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission));
|
||||
throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空",
|
||||
loginUser.getId(), tableName, tableAlias.getName()));
|
||||
}
|
||||
if (deptExpression == null) {
|
||||
return userExpression;
|
||||
}
|
||||
if (userExpression == null) {
|
||||
return deptExpression;
|
||||
}
|
||||
// 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE dept_id IN ? OR user_id = ?
|
||||
return new OrExpression(deptExpression, userExpression);
|
||||
}
|
||||
|
||||
private Expression buildDeptExpression(String tableName, Alias tableAlias, Set<Long> deptIds) {
|
||||
// 如果不存在配置,则无需作为条件
|
||||
String columnName = deptColumns.get(tableName);
|
||||
if (StrUtil.isEmpty(columnName)) {
|
||||
return null;
|
||||
}
|
||||
// 如果为空,则无条件
|
||||
if (CollUtil.isEmpty(deptIds)) {
|
||||
return null;
|
||||
}
|
||||
// 拼接条件
|
||||
return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName),
|
||||
new ExpressionList(CollectionUtils.convertList(deptIds, LongValue::new)));
|
||||
}
|
||||
|
||||
private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) {
|
||||
// 如果不查看自己,则无需作为条件
|
||||
if (Boolean.FALSE.equals(self)) {
|
||||
return null;
|
||||
}
|
||||
String columnName = userColumns.get(tableName);
|
||||
if (StrUtil.isEmpty(columnName)) {
|
||||
return null;
|
||||
}
|
||||
// 拼接条件
|
||||
return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(userId));
|
||||
}
|
||||
|
||||
// ==================== 添加配置 ====================
|
||||
|
||||
public void addDeptColumn(Class<? extends BaseDO> entityClass) {
|
||||
addDeptColumn(entityClass, DEPT_COLUMN_NAME);
|
||||
}
|
||||
|
||||
public void addDeptColumn(Class<? extends BaseDO> entityClass, String columnName) {
|
||||
String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName();
|
||||
addDeptColumn(tableName, columnName);
|
||||
}
|
||||
|
||||
public void addDeptColumn(String tableName, String columnName) {
|
||||
deptColumns.put(tableName, columnName);
|
||||
TABLE_NAMES.add(tableName);
|
||||
}
|
||||
|
||||
public void addUserColumn(Class<? extends BaseDO> entityClass) {
|
||||
addUserColumn(entityClass, USER_COLUMN_NAME);
|
||||
}
|
||||
|
||||
public void addUserColumn(Class<? extends BaseDO> entityClass, String columnName) {
|
||||
String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName();
|
||||
addUserColumn(tableName, columnName);
|
||||
}
|
||||
|
||||
public void addUserColumn(String tableName, String columnName) {
|
||||
userColumns.put(tableName, columnName);
|
||||
TABLE_NAMES.add(tableName);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.dept.rule;
|
||||
|
||||
/**
|
||||
* {@link DeptDataPermissionRule} 的自定义配置接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface DeptDataPermissionRuleCustomizer {
|
||||
|
||||
/**
|
||||
* 自定义该权限规则
|
||||
* 1. 调用 {@link DeptDataPermissionRule#addDeptColumn(Class, String)} 方法,配置基于 dept_id 的过滤规则
|
||||
* 2. 调用 {@link DeptDataPermissionRule#addUserColumn(Class, String)} 方法,配置基于 user_id 的过滤规则
|
||||
*
|
||||
* @param rule 权限规则
|
||||
*/
|
||||
void customize(DeptDataPermissionRule rule);
|
||||
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.dept.service;
|
||||
|
||||
import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO;
|
||||
import cn.iocoder.yudao.framework.security.core.LoginUser;
|
||||
|
||||
/**
|
||||
* 基于部门的数据权限 Framework Service 接口
|
||||
* 目前的实现类是 SysPermissionServiceImpl 类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface DeptDataPermissionFrameworkService {
|
||||
|
||||
/**
|
||||
* 获得登陆用户的部门数据权限
|
||||
*
|
||||
* @param loginUser 登陆用户
|
||||
* @return 部门数据权限
|
||||
*/
|
||||
DeptDataPermissionRespDTO getDeptDataPermission(LoginUser loginUser);
|
||||
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.dept.service.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 部门的数据权限 Response DTO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class DeptDataPermissionRespDTO {
|
||||
|
||||
/**
|
||||
* 是否可查看全部数据
|
||||
*/
|
||||
private Boolean all;
|
||||
/**
|
||||
* 是否可查看自己的数据
|
||||
*/
|
||||
private Boolean self;
|
||||
/**
|
||||
* 可查看的部门编号数组
|
||||
*/
|
||||
private Set<Long> deptIds;
|
||||
|
||||
public DeptDataPermissionRespDTO() {
|
||||
this.all = false;
|
||||
this.self = false;
|
||||
this.deptIds = new HashSet<>();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.rule;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import net.sf.jsqlparser.expression.Alias;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 数据权限规则接口
|
||||
* 通过实现接口,自定义数据规则。例如说,
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface DataPermissionRule {
|
||||
|
||||
/**
|
||||
* 返回需要生效的表名数组
|
||||
* 为什么需要该方法?Data Permission 数组基于 SQL 重写,通过 Where 返回只有权限的数据
|
||||
*
|
||||
* 如果需要基于实体名获得表名,可调用 {@link TableInfoHelper#getTableInfo(Class)} 获得
|
||||
*
|
||||
* @return 表名数组
|
||||
*/
|
||||
Set<String> getTableNames();
|
||||
|
||||
/**
|
||||
* 根据表名和别名,生成对应的 WHERE / OR 过滤条件
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param tableAlias 别名,可能为空
|
||||
* @return 过滤条件 Expression 表达式
|
||||
*/
|
||||
Expression getExpression(String tableName, Alias tableAlias);
|
||||
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.rule;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link DataPermissionRule} 工厂接口
|
||||
* 作为 {@link DataPermissionRule} 的容器,提供管理能力
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface DataPermissionRuleFactory {
|
||||
|
||||
/**
|
||||
* 获得所有数据权限规则数组
|
||||
*
|
||||
* @return 数据权限规则数组
|
||||
*/
|
||||
List<DataPermissionRule> getDataPermissionRules();
|
||||
|
||||
/**
|
||||
* 获得指定 Mapper 的数据权限规则数组
|
||||
*
|
||||
* @param mappedStatementId 指定 Mapper 的编号
|
||||
* @return 数据权限规则数组
|
||||
*/
|
||||
List<DataPermissionRule> getDataPermissionRule(String mappedStatementId);
|
||||
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.rule;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionContextHolder;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 默认的 DataPermissionRuleFactoryImpl 实现类
|
||||
* 支持通过 {@link DataPermissionContextHolder} 过滤数据权限
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class DataPermissionRuleFactoryImpl implements DataPermissionRuleFactory {
|
||||
|
||||
/**
|
||||
* 数据权限规则数组
|
||||
*/
|
||||
private final List<DataPermissionRule> rules;
|
||||
|
||||
@Override
|
||||
public List<DataPermissionRule> getDataPermissionRules() {
|
||||
return rules;
|
||||
}
|
||||
|
||||
@Override // mappedStatementId 参数,暂时没有用。以后,可以基于 mappedStatementId + DataPermission 进行缓存
|
||||
public List<DataPermissionRule> getDataPermissionRule(String mappedStatementId) {
|
||||
// 1. 无数据权限
|
||||
if (CollUtil.isEmpty(rules)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
// 2. 未配置,则默认开启
|
||||
DataPermission dataPermission = DataPermissionContextHolder.get();
|
||||
if (dataPermission == null) {
|
||||
return rules;
|
||||
}
|
||||
// 3. 已配置,但禁用
|
||||
if (!dataPermission.enable()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 4. 已配置,只选择部分规则
|
||||
if (ArrayUtil.isNotEmpty(dataPermission.includeRules())) {
|
||||
return rules.stream().filter(rule -> ArrayUtil.contains(dataPermission.includeRules(), rule.getClass()))
|
||||
.collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询
|
||||
}
|
||||
// 5. 已配置,只排除部分规则
|
||||
if (ArrayUtil.isNotEmpty(dataPermission.excludeRules())) {
|
||||
return rules.stream().filter(rule -> !ArrayUtil.contains(dataPermission.excludeRules(), rule.getClass()))
|
||||
.collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询
|
||||
}
|
||||
// 6. 已配置,全部规则
|
||||
return rules;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 基于 JSqlParser 解析 SQL,增加数据权限的 WHERE 条件
|
||||
*/
|
||||
package cn.iocoder.yudao.framework.datapermission;
|
@@ -0,0 +1,3 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
cn.iocoder.yudao.framework.datapermission.config.YudaoDataPermissionAutoConfiguration,\
|
||||
cn.iocoder.yudao.framework.datapermission.config.YudaoDeptDataPermissionAutoConfiguration
|
@@ -0,0 +1,108 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.aop;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link DataPermissionAnnotationInterceptor} 的单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class DataPermissionAnnotationInterceptorTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private DataPermissionAnnotationInterceptor interceptor;
|
||||
|
||||
@Mock
|
||||
private MethodInvocation methodInvocation;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
interceptor.getDataPermissionCache().clear();
|
||||
}
|
||||
|
||||
@Test // 无 @DataPermission 注解
|
||||
public void testInvoke_none() throws Throwable {
|
||||
// 参数
|
||||
mockMethodInvocation(TestNone.class);
|
||||
|
||||
// 调用
|
||||
Object result = interceptor.invoke(methodInvocation);
|
||||
// 断言
|
||||
assertEquals("none", result);
|
||||
assertEquals(1, interceptor.getDataPermissionCache().size());
|
||||
assertTrue(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable());
|
||||
}
|
||||
|
||||
@Test // 在 Method 上有 @DataPermission 注解
|
||||
public void testInvoke_method() throws Throwable {
|
||||
// 参数
|
||||
mockMethodInvocation(TestMethod.class);
|
||||
|
||||
// 调用
|
||||
Object result = interceptor.invoke(methodInvocation);
|
||||
// 断言
|
||||
assertEquals("method", result);
|
||||
assertEquals(1, interceptor.getDataPermissionCache().size());
|
||||
assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable());
|
||||
}
|
||||
|
||||
@Test // 在 Class 上有 @DataPermission 注解
|
||||
public void testInvoke_class() throws Throwable {
|
||||
// 参数
|
||||
mockMethodInvocation(TestClass.class);
|
||||
|
||||
// 调用
|
||||
Object result = interceptor.invoke(methodInvocation);
|
||||
// 断言
|
||||
assertEquals("class", result);
|
||||
assertEquals(1, interceptor.getDataPermissionCache().size());
|
||||
assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable());
|
||||
}
|
||||
|
||||
private void mockMethodInvocation(Class<?> clazz) throws Throwable {
|
||||
Object targetObject = clazz.newInstance();
|
||||
Method method = targetObject.getClass().getMethod("echo");
|
||||
when(methodInvocation.getThis()).thenReturn(targetObject);
|
||||
when(methodInvocation.getMethod()).thenReturn(method);
|
||||
when(methodInvocation.proceed()).then(invocationOnMock -> method.invoke(targetObject));
|
||||
}
|
||||
|
||||
static class TestMethod {
|
||||
|
||||
@DataPermission(enable = false)
|
||||
public String echo() {
|
||||
return "method";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@DataPermission(enable = false)
|
||||
static class TestClass {
|
||||
|
||||
public String echo() {
|
||||
return "class";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class TestNone {
|
||||
|
||||
public String echo() {
|
||||
return "none";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.aop;
|
||||
|
||||
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* {@link DataPermissionContextHolder} 的单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
class DataPermissionContextHolderTest {
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
DataPermissionContextHolder.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGet() {
|
||||
// mock 方法
|
||||
DataPermission dataPermission01 = mock(DataPermission.class);
|
||||
DataPermissionContextHolder.add(dataPermission01);
|
||||
DataPermission dataPermission02 = mock(DataPermission.class);
|
||||
DataPermissionContextHolder.add(dataPermission02);
|
||||
|
||||
// 调用
|
||||
DataPermission result = DataPermissionContextHolder.get();
|
||||
// 断言
|
||||
assertSame(result, dataPermission02);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPush() {
|
||||
// 调用
|
||||
DataPermission dataPermission01 = mock(DataPermission.class);
|
||||
DataPermissionContextHolder.add(dataPermission01);
|
||||
DataPermission dataPermission02 = mock(DataPermission.class);
|
||||
DataPermissionContextHolder.add(dataPermission02);
|
||||
// 断言
|
||||
DataPermission first = DataPermissionContextHolder.getAll().get(0);
|
||||
DataPermission second = DataPermissionContextHolder.getAll().get(1);
|
||||
assertSame(dataPermission01, first);
|
||||
assertSame(dataPermission02, second);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemove() {
|
||||
// mock 方法
|
||||
DataPermission dataPermission01 = mock(DataPermission.class);
|
||||
DataPermissionContextHolder.add(dataPermission01);
|
||||
DataPermission dataPermission02 = mock(DataPermission.class);
|
||||
DataPermissionContextHolder.add(dataPermission02);
|
||||
|
||||
// 调用
|
||||
DataPermission result = DataPermissionContextHolder.remove();
|
||||
// 断言
|
||||
assertSame(result, dataPermission02);
|
||||
assertEquals(1, DataPermissionContextHolder.getAll().size());
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,190 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.db;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
|
||||
import net.sf.jsqlparser.expression.Alias;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.LongValue;
|
||||
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
|
||||
import net.sf.jsqlparser.schema.Column;
|
||||
import org.apache.ibatis.executor.Executor;
|
||||
import org.apache.ibatis.executor.statement.StatementHandler;
|
||||
import org.apache.ibatis.mapping.BoundSql;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.util.*;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* {@link DataPermissionDatabaseInterceptor} 的单元测试
|
||||
* 主要测试 {@link DataPermissionDatabaseInterceptor#beforePrepare(StatementHandler, Connection, Integer)}
|
||||
* 和 {@link DataPermissionDatabaseInterceptor#beforeUpdate(Executor, MappedStatement, Object)}
|
||||
* 以及在这个过程中,ContextHolder 和 MappedStatementCache
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class DataPermissionDatabaseInterceptorTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private DataPermissionDatabaseInterceptor interceptor;
|
||||
|
||||
@Mock
|
||||
private DataPermissionRuleFactory ruleFactory;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
// 清理上下文
|
||||
DataPermissionDatabaseInterceptor.ContextHolder.clear();
|
||||
// 清空缓存
|
||||
interceptor.getMappedStatementCache().clear();
|
||||
}
|
||||
|
||||
@Test // 不存在规则,且不匹配
|
||||
public void testBeforeQuery_withoutRule() {
|
||||
try (MockedStatic<PluginUtils> pluginUtilsMock = mockStatic(PluginUtils.class)) {
|
||||
// 准备参数
|
||||
MappedStatement mappedStatement = mock(MappedStatement.class);
|
||||
BoundSql boundSql = mock(BoundSql.class);
|
||||
|
||||
// 调用
|
||||
interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
|
||||
// 断言
|
||||
pluginUtilsMock.verify(never(), () -> PluginUtils.mpBoundSql(boundSql));
|
||||
}
|
||||
}
|
||||
|
||||
@Test // 存在规则,且不匹配
|
||||
public void testBeforeQuery_withMatchRule() {
|
||||
try (MockedStatic<PluginUtils> pluginUtilsMock = mockStatic(PluginUtils.class)) {
|
||||
// 准备参数
|
||||
MappedStatement mappedStatement = mock(MappedStatement.class);
|
||||
BoundSql boundSql = mock(BoundSql.class);
|
||||
// mock 方法(数据权限)
|
||||
when(ruleFactory.getDataPermissionRule(same(mappedStatement.getId())))
|
||||
.thenReturn(singletonList(new DeptDataPermissionRule()));
|
||||
// mock 方法(MPBoundSql)
|
||||
PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class);
|
||||
pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs);
|
||||
// mock 方法(SQL)
|
||||
String sql = "select * from t_user where id = 1";
|
||||
when(mpBs.sql()).thenReturn(sql);
|
||||
// 针对 ContextHolder 和 MappedStatementCache 暂时不 mock,主要想校验过程中,数据是否正确
|
||||
|
||||
// 调用
|
||||
interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
|
||||
// 断言
|
||||
verify(mpBs, times(1)).sql(
|
||||
eq("SELECT * FROM t_user WHERE id = 1 AND dept_id = 100"));
|
||||
// 断言缓存
|
||||
assertTrue(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test // 存在规则,但不匹配
|
||||
public void testBeforeQuery_withoutMatchRule() {
|
||||
try (MockedStatic<PluginUtils> pluginUtilsMock = mockStatic(PluginUtils.class)) {
|
||||
// 准备参数
|
||||
MappedStatement mappedStatement = mock(MappedStatement.class);
|
||||
BoundSql boundSql = mock(BoundSql.class);
|
||||
// mock 方法(数据权限)
|
||||
when(ruleFactory.getDataPermissionRule(same(mappedStatement.getId())))
|
||||
.thenReturn(singletonList(new DeptDataPermissionRule()));
|
||||
// mock 方法(MPBoundSql)
|
||||
PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class);
|
||||
pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs);
|
||||
// mock 方法(SQL)
|
||||
String sql = "select * from t_role where id = 1";
|
||||
when(mpBs.sql()).thenReturn(sql);
|
||||
// 针对 ContextHolder 和 MappedStatementCache 暂时不 mock,主要想校验过程中,数据是否正确
|
||||
|
||||
// 调用
|
||||
interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
|
||||
// 断言
|
||||
verify(mpBs, times(1)).sql(
|
||||
eq("SELECT * FROM t_role WHERE id = 1"));
|
||||
// 断言缓存
|
||||
assertFalse(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddNoRewritable() {
|
||||
// 准备参数
|
||||
MappedStatement ms = mock(MappedStatement.class);
|
||||
List<DataPermissionRule> rules = singletonList(new DeptDataPermissionRule());
|
||||
// mock 方法
|
||||
when(ms.getId()).thenReturn("selectById");
|
||||
|
||||
// 调用
|
||||
interceptor.getMappedStatementCache().addNoRewritable(ms, rules);
|
||||
// 断言
|
||||
Map<Class<? extends DataPermissionRule>, Set<String>> noRewritableMappedStatements =
|
||||
interceptor.getMappedStatementCache().getNoRewritableMappedStatements();
|
||||
assertEquals(1, noRewritableMappedStatements.size());
|
||||
assertEquals(SetUtils.asSet("selectById"), noRewritableMappedStatements.get(DeptDataPermissionRule.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoRewritable() {
|
||||
// 准备参数
|
||||
MappedStatement ms = mock(MappedStatement.class);
|
||||
// mock 方法
|
||||
when(ms.getId()).thenReturn("selectById");
|
||||
// mock 数据
|
||||
List<DataPermissionRule> rules = singletonList(new DeptDataPermissionRule());
|
||||
interceptor.getMappedStatementCache().addNoRewritable(ms, rules);
|
||||
|
||||
// 场景一,rules 为空
|
||||
assertTrue(interceptor.getMappedStatementCache().noRewritable(ms, null));
|
||||
// 场景二,rules 非空,可重写
|
||||
assertFalse(interceptor.getMappedStatementCache().noRewritable(ms, singletonList(new EmptyDataPermissionRule())));
|
||||
// 场景三,rule 非空,不可重写
|
||||
assertTrue(interceptor.getMappedStatementCache().noRewritable(ms, rules));
|
||||
}
|
||||
|
||||
private static class DeptDataPermissionRule implements DataPermissionRule {
|
||||
|
||||
private static final String COLUMN = "dept_id";
|
||||
|
||||
@Override
|
||||
public Set<String> getTableNames() {
|
||||
return SetUtils.asSet("t_user");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression getExpression(String tableName, Alias tableAlias) {
|
||||
Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN);
|
||||
LongValue value = new LongValue(100L);
|
||||
return new EqualsTo(column, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class EmptyDataPermissionRule implements DataPermissionRule {
|
||||
|
||||
@Override
|
||||
public Set<String> getTableNames() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression getExpression(String tableName, Alias tableAlias) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,370 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.db;
|
||||
|
||||
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import net.sf.jsqlparser.expression.Alias;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.LongValue;
|
||||
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
|
||||
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
|
||||
import net.sf.jsqlparser.expression.operators.relational.InExpression;
|
||||
import net.sf.jsqlparser.schema.Column;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* {@link DataPermissionDatabaseInterceptor} 的单元测试
|
||||
* 主要复用了 MyBatis Plus 的 TenantLineInnerInterceptorTest 的单元测试
|
||||
* 不过它的单元测试不是很规范,考虑到是复用的,所以暂时不进行修改~
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class DataPermissionDatabaseInterceptorTest2 extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private DataPermissionDatabaseInterceptor interceptor;
|
||||
|
||||
@Mock
|
||||
private DataPermissionRuleFactory ruleFactory;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
// 租户的数据权限规则
|
||||
DataPermissionRule tenantRule = new DataPermissionRule() {
|
||||
|
||||
private static final String COLUMN = "tenant_id";
|
||||
|
||||
@Override
|
||||
public Set<String> getTableNames() {
|
||||
return asSet("entity", "entity1", "entity2", "t1", "t2", // 支持 MyBatis Plus 的单元测试
|
||||
"t_user", "t_role"); // 满足自己的单元测试
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression getExpression(String tableName, Alias tableAlias) {
|
||||
Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN);
|
||||
LongValue value = new LongValue(1L);
|
||||
return new EqualsTo(column, value);
|
||||
}
|
||||
|
||||
};
|
||||
// 部门的数据权限规则
|
||||
DataPermissionRule deptRule = new DataPermissionRule() {
|
||||
|
||||
private static final String COLUMN = "dept_id";
|
||||
|
||||
@Override
|
||||
public Set<String> getTableNames() {
|
||||
return asSet("t_user"); // 满足自己的单元测试
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression getExpression(String tableName, Alias tableAlias) {
|
||||
Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN);
|
||||
ExpressionList values = new ExpressionList(new LongValue(10L),
|
||||
new LongValue(20L));
|
||||
return new InExpression(column, values);
|
||||
}
|
||||
|
||||
};
|
||||
// 设置到上下文,保证
|
||||
DataPermissionDatabaseInterceptor.ContextHolder.init(Arrays.asList(tenantRule, deptRule));
|
||||
}
|
||||
|
||||
@Test
|
||||
void delete() {
|
||||
assertSql("delete from entity where id = ?",
|
||||
"DELETE FROM entity WHERE id = ? AND tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void update() {
|
||||
assertSql("update entity set name = ? where id = ?",
|
||||
"UPDATE entity SET name = ? WHERE id = ? AND tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectSingle() {
|
||||
// 单表
|
||||
assertSql("select * from entity where id = ?",
|
||||
"SELECT * FROM entity WHERE id = ? AND tenant_id = 1");
|
||||
|
||||
assertSql("select * from entity where id = ? or name = ?",
|
||||
"SELECT * FROM entity WHERE (id = ? OR name = ?) AND tenant_id = 1");
|
||||
|
||||
assertSql("SELECT * FROM entity WHERE (id = ? OR name = ?)",
|
||||
"SELECT * FROM entity WHERE (id = ? OR name = ?) AND tenant_id = 1");
|
||||
|
||||
/* not */
|
||||
assertSql("SELECT * FROM entity WHERE not (id = ? OR name = ?)",
|
||||
"SELECT * FROM entity WHERE NOT (id = ? OR name = ?) AND tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectSubSelectIn() {
|
||||
/* in */
|
||||
assertSql("SELECT * FROM entity e WHERE e.id IN (select e1.id from entity1 e1 where e1.id = ?)",
|
||||
"SELECT * FROM entity e WHERE e.id IN (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
|
||||
// 在最前
|
||||
assertSql("SELECT * FROM entity e WHERE e.id IN " +
|
||||
"(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?",
|
||||
"SELECT * FROM entity e WHERE e.id IN " +
|
||||
"(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1");
|
||||
// 在最后
|
||||
assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " +
|
||||
"(select e1.id from entity1 e1 where e1.id = ?)",
|
||||
"SELECT * FROM entity e WHERE e.id = ? AND e.id IN " +
|
||||
"(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
|
||||
// 在中间
|
||||
assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " +
|
||||
"(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?",
|
||||
"SELECT * FROM entity e WHERE e.id = ? AND e.id IN " +
|
||||
"(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectSubSelectEq() {
|
||||
/* = */
|
||||
assertSql("SELECT * FROM entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?)",
|
||||
"SELECT * FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectSubSelectInnerNotEq() {
|
||||
/* inner not = */
|
||||
assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?))",
|
||||
"SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1)) AND e.tenant_id = 1");
|
||||
|
||||
assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?) and e.id = ?)",
|
||||
"SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ?) AND e.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectSubSelectExists() {
|
||||
/* EXISTS */
|
||||
assertSql("SELECT * FROM entity e WHERE EXISTS (select e1.id from entity1 e1 where e1.id = ?)",
|
||||
"SELECT * FROM entity e WHERE EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
|
||||
|
||||
|
||||
/* NOT EXISTS */
|
||||
assertSql("SELECT * FROM entity e WHERE NOT EXISTS (select e1.id from entity1 e1 where e1.id = ?)",
|
||||
"SELECT * FROM entity e WHERE NOT EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectSubSelect() {
|
||||
/* >= */
|
||||
assertSql("SELECT * FROM entity e WHERE e.id >= (select e1.id from entity1 e1 where e1.id = ?)",
|
||||
"SELECT * FROM entity e WHERE e.id >= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
|
||||
|
||||
/* <= */
|
||||
assertSql("SELECT * FROM entity e WHERE e.id <= (select e1.id from entity1 e1 where e1.id = ?)",
|
||||
"SELECT * FROM entity e WHERE e.id <= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
|
||||
|
||||
/* <> */
|
||||
assertSql("SELECT * FROM entity e WHERE e.id <> (select e1.id from entity1 e1 where e1.id = ?)",
|
||||
"SELECT * FROM entity e WHERE e.id <> (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectFromSelect() {
|
||||
assertSql("SELECT * FROM (select e.id from entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?))",
|
||||
"SELECT * FROM (SELECT e.id FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectBodySubSelect() {
|
||||
assertSql("select t1.col1,(select t2.col2 from t2 t2 where t1.col1=t2.col1) from t1 t1",
|
||||
"SELECT t1.col1, (SELECT t2.col2 FROM t2 t2 WHERE t1.col1 = t2.col1 AND t2.tenant_id = 1) FROM t1 t1 WHERE t1.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectLeftJoin() {
|
||||
// left join
|
||||
assertSql("SELECT * FROM entity e " +
|
||||
"left join entity1 e1 on e1.id = e.id " +
|
||||
"WHERE e.id = ? OR e.name = ?",
|
||||
"SELECT * FROM entity e " +
|
||||
"LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
|
||||
|
||||
assertSql("SELECT * FROM entity e " +
|
||||
"left join entity1 e1 on e1.id = e.id " +
|
||||
"WHERE (e.id = ? OR e.name = ?)",
|
||||
"SELECT * FROM entity e " +
|
||||
"LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectRightJoin() {
|
||||
// right join
|
||||
assertSql("SELECT * FROM entity e " +
|
||||
"right join entity1 e1 on e1.id = e.id",
|
||||
"SELECT * FROM entity e " +
|
||||
"RIGHT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"WHERE e.tenant_id = 1");
|
||||
|
||||
assertSql("SELECT * FROM entity e " +
|
||||
"right join entity1 e1 on e1.id = e.id " +
|
||||
"WHERE e.id = ? OR e.name = ?",
|
||||
"SELECT * FROM entity e " +
|
||||
"RIGHT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectLeftJoinMultipleTrailingOn() {
|
||||
// 多个 on 尾缀的
|
||||
assertSql("SELECT * FROM entity e " +
|
||||
"LEFT JOIN entity1 e1 " +
|
||||
"LEFT JOIN entity2 e2 ON e2.id = e1.id " +
|
||||
"ON e1.id = e.id " +
|
||||
"WHERE (e.id = ? OR e.NAME = ?)",
|
||||
"SELECT * FROM entity e " +
|
||||
"LEFT JOIN entity1 e1 " +
|
||||
"LEFT JOIN entity2 e2 ON e2.id = e1.id AND e2.tenant_id = 1 " +
|
||||
"ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1");
|
||||
|
||||
assertSql("SELECT * FROM entity e " +
|
||||
"LEFT JOIN entity1 e1 " +
|
||||
"LEFT JOIN with_as_A e2 ON e2.id = e1.id " +
|
||||
"ON e1.id = e.id " +
|
||||
"WHERE (e.id = ? OR e.NAME = ?)",
|
||||
"SELECT * FROM entity e " +
|
||||
"LEFT JOIN entity1 e1 " +
|
||||
"LEFT JOIN with_as_A e2 ON e2.id = e1.id " +
|
||||
"ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectInnerJoin() {
|
||||
// inner join
|
||||
assertSql("SELECT * FROM entity e " +
|
||||
"inner join entity1 e1 on e1.id = e.id " +
|
||||
"WHERE e.id = ? OR e.name = ?",
|
||||
"SELECT * FROM entity e " +
|
||||
"INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
|
||||
|
||||
assertSql("SELECT * FROM entity e " +
|
||||
"inner join entity1 e1 on e1.id = e.id " +
|
||||
"WHERE (e.id = ? OR e.name = ?)",
|
||||
"SELECT * FROM entity e " +
|
||||
"INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
|
||||
|
||||
// 垃圾 inner join todo
|
||||
// assertSql("SELECT * FROM entity,entity1 " +
|
||||
// "WHERE entity.id = entity1.id",
|
||||
// "SELECT * FROM entity e " +
|
||||
// "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
// "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectWithAs() {
|
||||
assertSql("with with_as_A as (select * from entity) select * from with_as_A",
|
||||
"WITH with_as_A AS (SELECT * FROM entity WHERE tenant_id = 1) SELECT * FROM with_as_A");
|
||||
}
|
||||
|
||||
private void assertSql(String sql, String targetSql) {
|
||||
assertEquals(targetSql, interceptor.parserSingle(sql, null));
|
||||
}
|
||||
|
||||
// ========== 额外的测试 ==========
|
||||
|
||||
@Test
|
||||
public void testSelectSingle() {
|
||||
// 单表
|
||||
assertSql("select * from t_user where id = ?",
|
||||
"SELECT * FROM t_user WHERE id = ? AND tenant_id = 1 AND dept_id IN (10, 20)");
|
||||
|
||||
assertSql("select * from t_user where id = ? or name = ?",
|
||||
"SELECT * FROM t_user WHERE (id = ? OR name = ?) AND tenant_id = 1 AND dept_id IN (10, 20)");
|
||||
|
||||
assertSql("SELECT * FROM t_user WHERE (id = ? OR name = ?)",
|
||||
"SELECT * FROM t_user WHERE (id = ? OR name = ?) AND tenant_id = 1 AND dept_id IN (10, 20)");
|
||||
|
||||
/* not */
|
||||
assertSql("SELECT * FROM t_user WHERE not (id = ? OR name = ?)",
|
||||
"SELECT * FROM t_user WHERE NOT (id = ? OR name = ?) AND tenant_id = 1 AND dept_id IN (10, 20)");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectLeftJoin() {
|
||||
// left join
|
||||
assertSql("SELECT * FROM t_user e " +
|
||||
"left join t_role e1 on e1.id = e.id " +
|
||||
"WHERE e.id = ? OR e.name = ?",
|
||||
"SELECT * FROM t_user e " +
|
||||
"LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)");
|
||||
|
||||
// 条件 e.id = ? OR e.name = ? 带括号
|
||||
assertSql("SELECT * FROM t_user e " +
|
||||
"left join t_role e1 on e1.id = e.id " +
|
||||
"WHERE (e.id = ? OR e.name = ?)",
|
||||
"SELECT * FROM t_user e " +
|
||||
"LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectRightJoin() {
|
||||
// right join
|
||||
assertSql("SELECT * FROM t_user e " +
|
||||
"right join t_role e1 on e1.id = e.id " +
|
||||
"WHERE e.id = ? OR e.name = ?",
|
||||
"SELECT * FROM t_user e " +
|
||||
"RIGHT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)");
|
||||
|
||||
// 条件 e.id = ? OR e.name = ? 带括号
|
||||
assertSql("SELECT * FROM t_user e " +
|
||||
"right join t_role e1 on e1.id = e.id " +
|
||||
"WHERE (e.id = ? OR e.name = ?)",
|
||||
"SELECT * FROM t_user e " +
|
||||
"RIGHT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectInnerJoin() {
|
||||
// inner join
|
||||
assertSql("SELECT * FROM t_user e " +
|
||||
"inner join entity1 e1 on e1.id = e.id " +
|
||||
"WHERE e.id = ? OR e.name = ?",
|
||||
"SELECT * FROM t_user e " +
|
||||
"INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)");
|
||||
|
||||
// 条件 e.id = ? OR e.name = ? 带括号
|
||||
assertSql("SELECT * FROM t_user e " +
|
||||
"inner join t_role e1 on e1.id = e.id " +
|
||||
"WHERE (e.id = ? OR e.name = ?)",
|
||||
"SELECT * FROM t_user e " +
|
||||
"INNER JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)");
|
||||
|
||||
// 垃圾 inner join todo
|
||||
// assertSql("SELECT * FROM entity,entity1 " +
|
||||
// "WHERE entity.id = entity1.id",
|
||||
// "SELECT * FROM entity e " +
|
||||
// "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||
// "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,221 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.dept.rule;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.dept.service.DeptDataPermissionFrameworkService;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO;
|
||||
import cn.iocoder.yudao.framework.security.core.LoginUser;
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import net.sf.jsqlparser.expression.Alias;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link DeptDataPermissionRule} 的单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
class DeptDataPermissionRuleTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private DeptDataPermissionRule rule;
|
||||
|
||||
@Mock
|
||||
private DeptDataPermissionFrameworkService deptDataPermissionFrameworkService;
|
||||
|
||||
@BeforeEach
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setUp() {
|
||||
// 清空 rule
|
||||
rule.getTableNames().clear();
|
||||
((Map<String, String>) ReflectUtil.getFieldValue(rule, "deptColumns")).clear();
|
||||
((Map<String, String>) ReflectUtil.getFieldValue(rule, "deptColumns")).clear();
|
||||
}
|
||||
|
||||
@Test // 无 LoginUser
|
||||
public void testGetExpression_noLoginUser() {
|
||||
// 准备参数
|
||||
String tableName = randomString();
|
||||
Alias tableAlias = new Alias(randomString());
|
||||
// mock 方法
|
||||
|
||||
// 调用
|
||||
Expression expression = rule.getExpression(tableName, tableAlias);
|
||||
// 断言
|
||||
assertNull(expression);
|
||||
}
|
||||
|
||||
@Test // 无数据权限时
|
||||
public void testGetExpression_noDeptDataPermission() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
|
||||
= mockStatic(SecurityFrameworkUtils.class)) {
|
||||
// 准备参数
|
||||
String tableName = "t_user";
|
||||
Alias tableAlias = new Alias("u");
|
||||
// mock 方法
|
||||
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L));
|
||||
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
|
||||
|
||||
// 调用
|
||||
NullPointerException exception = assertThrows(NullPointerException.class,
|
||||
() -> rule.getExpression(tableName, tableAlias));
|
||||
// 断言
|
||||
assertEquals("LoginUser(1) Table(t_user/u) 未返回数据权限", exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test // 全部数据权限
|
||||
public void testGetExpression_allDeptDataPermission() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
|
||||
= mockStatic(SecurityFrameworkUtils.class)) {
|
||||
// 准备参数
|
||||
String tableName = "t_user";
|
||||
Alias tableAlias = new Alias("u");
|
||||
// mock 方法(LoginUser)
|
||||
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L));
|
||||
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
|
||||
// mock 方法(DeptDataPermissionRespDTO)
|
||||
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO().setAll(true);
|
||||
when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission);
|
||||
|
||||
// 调用
|
||||
Expression expression = rule.getExpression(tableName, tableAlias);
|
||||
// 断言
|
||||
assertNull(expression);
|
||||
}
|
||||
}
|
||||
|
||||
@Test // 即不能查看部门,又不能查看自己,则说明 100% 无权限
|
||||
public void testGetExpression_noDept_noSelf() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
|
||||
= mockStatic(SecurityFrameworkUtils.class)) {
|
||||
// 准备参数
|
||||
String tableName = "t_user";
|
||||
Alias tableAlias = new Alias("u");
|
||||
// mock 方法(LoginUser)
|
||||
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L));
|
||||
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
|
||||
// mock 方法(DeptDataPermissionRespDTO)
|
||||
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO();
|
||||
when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission);
|
||||
|
||||
// 调用
|
||||
Expression expression = rule.getExpression(tableName, tableAlias);
|
||||
// 断言
|
||||
assertEquals("null = null", expression.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Test // 拼接 Dept 和 User 的条件(字段都不符合)
|
||||
public void testGetExpression_noDeptColumn_noSelfColumn() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
|
||||
= mockStatic(SecurityFrameworkUtils.class)) {
|
||||
// 准备参数
|
||||
String tableName = "t_user";
|
||||
Alias tableAlias = new Alias("u");
|
||||
// mock 方法(LoginUser)
|
||||
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L));
|
||||
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
|
||||
// mock 方法(DeptDataPermissionRespDTO)
|
||||
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
|
||||
.setDeptIds(SetUtils.asSet(10L, 20L)).setSelf(true);
|
||||
when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission);
|
||||
|
||||
// 调用
|
||||
NullPointerException exception = assertThrows(NullPointerException.class,
|
||||
() -> rule.getExpression(tableName, tableAlias));
|
||||
// 断言
|
||||
assertEquals("LoginUser(1) Table(t_user/u) 构建的条件为空", exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test // 拼接 Dept 和 User 的条件(self 符合)
|
||||
public void testGetExpression_noDeptColumn_yesSelfColumn() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
|
||||
= mockStatic(SecurityFrameworkUtils.class)) {
|
||||
// 准备参数
|
||||
String tableName = "t_user";
|
||||
Alias tableAlias = new Alias("u");
|
||||
// mock 方法(LoginUser)
|
||||
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L));
|
||||
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
|
||||
// mock 方法(DeptDataPermissionRespDTO)
|
||||
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
|
||||
.setSelf(true);
|
||||
when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission);
|
||||
// 添加 user 字段配置
|
||||
rule.addUserColumn("t_user", "id");
|
||||
|
||||
// 调用
|
||||
Expression expression = rule.getExpression(tableName, tableAlias);
|
||||
// 断言
|
||||
assertEquals("u.id = 1", expression.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Test // 拼接 Dept 和 User 的条件(dept 符合)
|
||||
public void testGetExpression_yesDeptColumn_noSelfColumn() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
|
||||
= mockStatic(SecurityFrameworkUtils.class)) {
|
||||
// 准备参数
|
||||
String tableName = "t_user";
|
||||
Alias tableAlias = new Alias("u");
|
||||
// mock 方法(LoginUser)
|
||||
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L));
|
||||
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
|
||||
// mock 方法(DeptDataPermissionRespDTO)
|
||||
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
|
||||
.setDeptIds(CollUtil.newLinkedHashSet(10L, 20L));
|
||||
when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission);
|
||||
// 添加 dept 字段配置
|
||||
rule.addDeptColumn("t_user", "dept_id");
|
||||
|
||||
// 调用
|
||||
Expression expression = rule.getExpression(tableName, tableAlias);
|
||||
// 断言
|
||||
assertEquals("u.dept_id IN (10, 20)", expression.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Test // 拼接 Dept 和 User 的条件(dept + self 符合)
|
||||
public void testGetExpression_yesDeptColumn_yesSelfColumn() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
|
||||
= mockStatic(SecurityFrameworkUtils.class)) {
|
||||
// 准备参数
|
||||
String tableName = "t_user";
|
||||
Alias tableAlias = new Alias("u");
|
||||
// mock 方法(LoginUser)
|
||||
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L));
|
||||
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
|
||||
// mock 方法(DeptDataPermissionRespDTO)
|
||||
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
|
||||
.setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)).setSelf(true);
|
||||
when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission);
|
||||
// 添加 user 字段配置
|
||||
rule.addUserColumn("t_user", "id");
|
||||
// 添加 dept 字段配置
|
||||
rule.addDeptColumn("t_user", "dept_id");
|
||||
|
||||
// 调用
|
||||
Expression expression = rule.getExpression(tableName, tableAlias);
|
||||
// 断言
|
||||
assertEquals("u.dept_id IN (10, 20) OR u.id = 1", expression.toString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,145 @@
|
||||
package cn.iocoder.yudao.framework.datapermission.core.rule;
|
||||
|
||||
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionContextHolder;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import net.sf.jsqlparser.expression.Alias;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Spy;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* {@link DataPermissionRuleFactoryImpl} 单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
class DataPermissionRuleFactoryImplTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private DataPermissionRuleFactoryImpl dataPermissionRuleFactory;
|
||||
|
||||
@Spy
|
||||
private List<DataPermissionRule> rules = Arrays.asList(new DataPermissionRule01(),
|
||||
new DataPermissionRule02());
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
DataPermissionContextHolder.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDataPermissionRule_02() {
|
||||
// 准备参数
|
||||
String mappedStatementId = randomString();
|
||||
|
||||
// 调用
|
||||
List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
|
||||
// 断言
|
||||
assertSame(rules, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDataPermissionRule_03() {
|
||||
// 准备参数
|
||||
String mappedStatementId = randomString();
|
||||
// mock 方法
|
||||
DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass03.class, DataPermission.class));
|
||||
|
||||
// 调用
|
||||
List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
|
||||
// 断言
|
||||
assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDataPermissionRule_04() {
|
||||
// 准备参数
|
||||
String mappedStatementId = randomString();
|
||||
// mock 方法
|
||||
DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass04.class, DataPermission.class));
|
||||
|
||||
// 调用
|
||||
List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
|
||||
// 断言
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(DataPermissionRule01.class, result.get(0).getClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDataPermissionRule_05() {
|
||||
// 准备参数
|
||||
String mappedStatementId = randomString();
|
||||
// mock 方法
|
||||
DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass05.class, DataPermission.class));
|
||||
|
||||
// 调用
|
||||
List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
|
||||
// 断言
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(DataPermissionRule02.class, result.get(0).getClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDataPermissionRule_06() {
|
||||
// 准备参数
|
||||
String mappedStatementId = randomString();
|
||||
// mock 方法
|
||||
DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass06.class, DataPermission.class));
|
||||
|
||||
// 调用
|
||||
List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
|
||||
// 断言
|
||||
assertSame(rules, result);
|
||||
}
|
||||
|
||||
@DataPermission(enable = false)
|
||||
static class TestClass03 {}
|
||||
|
||||
@DataPermission(includeRules = DataPermissionRule01.class)
|
||||
static class TestClass04 {}
|
||||
|
||||
@DataPermission(excludeRules = DataPermissionRule01.class)
|
||||
static class TestClass05 {}
|
||||
|
||||
@DataPermission
|
||||
static class TestClass06 {}
|
||||
|
||||
static class DataPermissionRule01 implements DataPermissionRule {
|
||||
|
||||
@Override
|
||||
public Set<String> getTableNames() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression getExpression(String tableName, Alias tableAlias) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class DataPermissionRule02 implements DataPermissionRule {
|
||||
|
||||
@Override
|
||||
public Set<String> getTableNames() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression getExpression(String tableName, Alias tableAlias) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user