How to Implement Data Permission Filtering Based on Role Hierarchy in ContiNew Admin
ContiNew Admin implements role-based data permission filtering by combining a configurable DataScopeEnum on roles, a MyBatis interceptor that dynamically rewrites SQL queries, and a department hierarchy tracked via an ancestors column to automatically restrict data visibility based on the user's role assignments.
ContiNew Admin provides an enterprise-grade data permission mechanism that eliminates the need for manual SQL writing in business code. By leveraging the starter-extension-datapermission module, the framework automatically injects data-scoping conditions into database queries based on the hierarchical relationships between users, roles, and departments.
Understanding the Data Scope Enumeration
The foundation of the permission system lies in the DataScopeEnum defined in top.continew.admin.common.enums.DataScopeEnum and stored within the RoleDO entity at continew-system/src/main/java/top/continew/admin/system/model/entity/RoleDO.java.
Each role declares a dataScope field that determines the visibility boundary:
- ALL: No restriction; access to all data.
- DEPT_AND_CHILD: Access to the user's own department and all descendant departments.
- DEPT: Access restricted to the user's immediate department only.
- SELF: Access restricted to records created by the user.
- CUSTOM: Application-specific logic defined by developers.
@Data
@TableName("sys_role")
public class RoleDO extends BaseDO {
/** 角色名称 */
private String name;
/** 编码 */
private String code;
/** 数据权限范围(ALL、DEPT_AND_CHILD、DEPT、SELF、CUSTOM) */
private DataScopeEnum dataScope;
}
When a role is created or updated, the dataScope value is persisted to the database and subsequently loaded into the runtime user context upon authentication.
Configuring Data Permissions on Roles
To implement filtering, first assign appropriate data scopes to roles during creation. In RoleServiceImpl.java, the system provides methods like create() and assignToUsers() to manage these assignments.
// Create a role with department-and-children scope
RoleReq roleReq = new RoleReq();
roleReq.setName("Regional Manager");
roleReq.setCode("REGION_MANAGER");
roleReq.setDataScope(DataScopeEnum.DEPT_AND_CHILD);
Long roleId = roleService.create(roleReq);
// Assign the role to a user
roleService.assignToUsers(roleId, List.of(userId));
A user may hold multiple roles simultaneously. The framework aggregates all applicable data scopes from the user's role collection and combines them with OR logic when generating SQL filters.
Runtime Context and User Data Provider
During authentication, the AbstractLoginHandler populates UserContext (located at continew-common/src/main/java/top/continew/admin/common/context/UserContext.java) with the user's ID, department ID, and a list of RoleContext objects containing each role's data scope. The DefaultDataPermissionUserDataProvider at continew-common/src/main/java/top/continew/admin/common/config/mybatis/DefaultDataPermissionUserDataProvider.java extracts this information to supply UserData to the MyBatis interceptor.
UserContext userContext = UserContextHolder.getContext();
UserData userData = new UserData();
userData.setUserId(userContext.getId());
userData.setDeptId(userContext.getDeptId());
userData.setRoles(
CollUtils.mapToSet(userContext.getRoles(),
r -> new RoleData(r.getId(), DataScope.valueOf(r.getDataScope().name()))));
The provider includes a critical bypass mechanism: if the current user is a Super Admin or Tenant Admin, the isFilter() method returns false, causing the interceptor to skip SQL modification entirely and grant full data access.
The MyBatis Interceptor and @DataPermission Annotation
The actual SQL rewriting occurs in the MyBatis interceptor provided by starter-extension-datapermission. This interceptor targets methods annotated with @DataPermission. All base mappers extend DataPermissionMapper (located at continew-common/src/main/java/top/continew/admin/common/base/mapper/DataPermissionMapper.java), which already annotates common CRUD methods like selectList and deleteById.
For custom queries, apply the annotation to any mapper method:
public interface OrderMapper extends DataPermissionMapper<OrderDO> {
@DataPermission
List<OrderDO> selectPending(@Param("status") String status);
}
The interceptor translates each DataScopeEnum into specific SQL fragments:
- DEPT:
dept_id = #{userDeptId} - DEPT_AND_CHILD:
dept_id IN (SELECT id FROM sys_dept WHERE FIND_IN_SET(#{userDeptId}, ancestors) OR id = #{userDeptId}) - SELF:
user_id = #{userId} - ALL: No condition appended
These fragments are combined with OR operators and wrapped in a top-level AND (...) clause, preserving the original query logic while appending permission constraints.
Managing Department Hierarchy for DEPT_AND_CHILD Scope
The DEPT_AND_CHILD scope relies on the department hierarchy maintained in DeptDO.java at continew-system/src/main/java/top/continew/admin/system/model/entity/DeptDO.java. Each department stores an ancestors column containing a comma-separated list of parent department IDs.
The DeptServiceImpl.java at continew-system/src/main/java/top/continew/admin/system/service/impl/DeptServiceImpl.java manages this hierarchy:
// When creating a department
req.setAncestors(this.getAncestors(req.getParentId()));
// When moving a department
String newAncestors = this.getAncestors(req.getParentId());
req.setAncestors(newAncestors);
this.updateChildrenAncestors(newAncestors, oldDept.getAncestors(), id);
The interceptor utilizes the same FIND_IN_SET(id, ancestors) logic used by DeptServiceImpl.listChildren() to identify all descendant departments dynamically.
Practical Implementation Examples
Querying Data with Automatic Filtering
When a controller invokes a mapped method, the interceptor automatically injects the appropriate WHERE clause based on the user's roles:
@RestController
@RequiredArgsConstructor
public class OrderController {
private final OrderMapper orderMapper;
@GetMapping("/orders")
public List<OrderVO> list(@RequestParam String status) {
// The interceptor automatically appends data permission conditions
return orderMapper.selectPending(status);
}
}
Creating a Custom Data Permission Provider
To override default behavior for specific scenarios (such as service accounts), implement a custom provider:
@Service
public class CustomDataPermissionProvider implements DataPermissionUserDataProvider {
@Override
public boolean isFilter() {
// Disable filtering for internal service accounts
return !UserContextHolder.getContext().getUsername().equals("internal-service");
}
@Override
public UserData getUserData() {
// Custom logic for determining user data
return new UserData();
}
}
Register this as a Spring bean to replace the DefaultDataPermissionUserDataProvider.
Summary
- Role Configuration: Define data scope via
DataScopeEnuminRoleDO.java(ALL, DEPT_AND_CHILD, DEPT, SELF, CUSTOM). - Context Population:
UserContextandDefaultDataPermissionUserDataProvidersupply user ID, department ID, and role scopes to the interceptor. - SQL Rewriting: The MyBatis interceptor processes
@DataPermissionannotations and injects OR-combined WHERE clauses based on the aggregated role scopes. - Hierarchy Support: The
ancestorscolumn inDeptDO.javaenables theDEPT_AND_CHILDscope to dynamically include all sub-departments. - Admin Bypass: Super Admins and Tenant Admins bypass filtering automatically via
isFilter()checks.
Frequently Asked Questions
How does ContiNew Admin handle users with multiple roles having different data scopes?
The framework aggregates all data scopes from the user's assigned roles and combines them using OR logic in the SQL WHERE clause. For example, if a user has one role with DEPT scope and another with SELF scope, the generated SQL includes AND (dept_id = #{deptId} OR user_id = #{userId}), granting access to both their department's records and their own records.
What SQL is generated for the DEPT_AND_CHILD data scope?
The interceptor generates a subquery utilizing the ancestors column: dept_id IN (SELECT id FROM sys_dept WHERE FIND_IN_SET(#{userDeptId}, ancestors) OR id = #{userDeptId}). This query identifies all departments where the user's department ID appears in the ancestry chain, effectively capturing the entire organizational branch beneath the user's department.
Can data permission filtering be disabled for specific API endpoints?
Yes, filtering can be bypassed by ensuring the method is not annotated with @DataPermission. Since DataPermissionMapper provides base CRUD methods that include the annotation, create custom mapper methods without the annotation for endpoints requiring unrestricted access. Alternatively, override DefaultDataPermissionUserDataProvider.isFilter() to return false based on custom conditions such as specific user types or request contexts.
How does the system maintain department hierarchy integrity when departments are moved?
DeptServiceImpl.java handles hierarchy updates via the updateChildrenAncestors() method. When a department's parent changes, the system recalculates the ancestors value for the moved department and cascades updates to all children, ensuring the comma-separated parent ID list remains accurate for data permission queries.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →