diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index d8d982ba3..980e09705 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -40,6 +40,7 @@ 7.2.6.RELEASE 0.1.16 + 3.6.28 1.18.20 1.4.1.Final @@ -312,6 +313,13 @@ cn.iocoder.boot yudao-spring-boot-starter-test ${revision} + test + + + + org.mockito + mockito-inline + ${mockito-inline.version} @@ -323,6 +331,10 @@ asm org.ow2.asm + + org.mockito + mockito-core + diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/interceptor/DataPermissionInterceptor.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/interceptor/DataPermissionInterceptor.java index c3cc4d9d9..e7fd69bac 100644 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/interceptor/DataPermissionInterceptor.java +++ b/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/interceptor/DataPermissionInterceptor.java @@ -10,6 +10,7 @@ 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; @@ -48,6 +49,7 @@ public class DataPermissionInterceptor extends JsqlParserSupport implements Inne private final DataPermissionRuleFactory ruleFactory; + @Getter private final MappedStatementCache mappedStatementCache = new MappedStatementCache(); @Override // SELECT 场景 @@ -442,13 +444,14 @@ public class DataPermissionInterceptor extends JsqlParserSupport implements Inne * * @author 芋道源码 */ - private static final class MappedStatementCache { + static final class MappedStatementCache { /** - * 无需重写的映射 + * 指定数据权限规则,对指定 MappedStatement 无需重写(不生效)的缓存 * * value:{@link MappedStatement#getId()} 编号 */ + @Getter private final Map, Set> noRewritableMappedStatements = new ConcurrentHashMap<>(); /** @@ -467,7 +470,7 @@ public class DataPermissionInterceptor extends JsqlParserSupport implements Inne // 任一规则不在 noRewritableMap 中,则说明可能需要重写 for (DataPermissionRule rule : rules) { Set mappedStatementIds = noRewritableMappedStatements.get(rule.getClass()); - if (!CollUtil.contains(mappedStatementIds, ms.getId())) { // 不存在,则说明可能要重写 + if (!CollUtil.contains(mappedStatementIds, ms.getId())) { return false; } } @@ -491,6 +494,14 @@ public class DataPermissionInterceptor extends JsqlParserSupport implements Inne } } + /** + * 清空缓存 + * 目前主要提供给单元测试 + */ + public void clear() { + noRewritableMappedStatements.clear(); + } + } } diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/interceptor/DataPermissionInterceptorTest.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/interceptor/DataPermissionInterceptorTest.java new file mode 100644 index 000000000..3946f5a49 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/interceptor/DataPermissionInterceptorTest.java @@ -0,0 +1,190 @@ +package cn.iocoder.yudao.framework.datapermission.core.interceptor; + +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 DataPermissionInterceptor} 的单元测试 + * 主要测试 {@link DataPermissionInterceptor#beforePrepare(StatementHandler, Connection, Integer)} + * 和 {@link DataPermissionInterceptor#beforeUpdate(Executor, MappedStatement, Object)} + * 以及在这个过程中,ContextHolder 和 MappedStatementCache + * + * @author 芋道源码 + */ +public class DataPermissionInterceptorTest extends BaseMockitoUnitTest { + + @InjectMocks + private DataPermissionInterceptor interceptor; + + @Mock + private DataPermissionRuleFactory ruleFactory; + + @BeforeEach + public void setUp() { + // 清理上下文 + DataPermissionInterceptor.ContextHolder.clear(); + // 清空缓存 + interceptor.getMappedStatementCache().clear(); + } + + @Test // 不存在规则,且不匹配 + public void testBeforeQuery_withoutRule() { + try (MockedStatic 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 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 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 rules = singletonList(new DeptDataPermissionRule()); + // mock 方法 + when(ms.getId()).thenReturn("selectById"); + + // 调用 + interceptor.getMappedStatementCache().addNoRewritable(ms, rules); + // 断言 + Map, Set> 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 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 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 getTableNames() { + return Collections.emptySet(); + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + return null; + } + + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-test/pom.xml b/yudao-framework/yudao-spring-boot-starter-test/pom.xml index 9500403d9..0b211f3cd 100644 --- a/yudao-framework/yudao-spring-boot-starter-test/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-test/pom.xml @@ -22,6 +22,10 @@ + + org.mockito + mockito-inline + org.springframework.boot spring-boot-starter-test diff --git a/更新日志.md b/更新日志.md index 266eacbbd..29f109c3d 100644 --- a/更新日志.md +++ b/更新日志.md @@ -22,11 +22,12 @@ ### ⚠️ Warning -这是一个多租户的预览版本,涉及的改动较大。 +这个版本新增了多租户与数据权限两个重量级的功能,建议花点时间进行了解与学习。 ### ⭐ New Features * 【新增】多租户,支持 Web、Security、Job、MQ、Async、DB、Redis 组件 +* 【新增】数据权限,内置基于部门过滤的规则 * 【新增】用户前台的昵称、头像的修改 ### 🐞 Bug Fixes @@ -35,7 +36,7 @@ ### 🔨 Dependency Upgrades -暂无 +* 【引入】mockito-inline 3.6.28:Mockito 提供对 final、static 的支持 ### 📝 TODO