# How to Implement Data Permission Filtering Based on Role Hierarchy in ContiNew Admin

> Learn how to implement data permission filtering based on role hierarchy in ContiNew Admin. Discover how to dynamically restrict data visibility using role scope and SQL query rewriting.

- Repository: [OpenContiNew/continew-admin](https://github.com/continew-org/continew-admin)
- Tags: how-to-guide
- Published: 2026-02-27

---

**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`](https://github.com/continew-org/continew-admin/blob/main/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.

```java
@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`](https://github.com/continew-org/continew-admin/blob/main/RoleServiceImpl.java), the system provides methods like `create()` and `assignToUsers()` to manage these assignments.

```java
// 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`](https://github.com/continew-org/continew-admin/blob/main/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`](https://github.com/continew-org/continew-admin/blob/main/continew-common/src/main/java/top/continew/admin/common/config/mybatis/DefaultDataPermissionUserDataProvider.java) extracts this information to supply `UserData` to the MyBatis interceptor.

```java
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`](https://github.com/continew-org/continew-admin/blob/main/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:

```java
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`](https://github.com/continew-org/continew-admin/blob/main/DeptDO.java) at [`continew-system/src/main/java/top/continew/admin/system/model/entity/DeptDO.java`](https://github.com/continew-org/continew-admin/blob/main/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`](https://github.com/continew-org/continew-admin/blob/main/DeptServiceImpl.java) at [`continew-system/src/main/java/top/continew/admin/system/service/impl/DeptServiceImpl.java`](https://github.com/continew-org/continew-admin/blob/main/continew-system/src/main/java/top/continew/admin/system/service/impl/DeptServiceImpl.java) manages this hierarchy:

```java
// 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:

```java
@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:

```java
@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 `DataScopeEnum` in [`RoleDO.java`](https://github.com/continew-org/continew-admin/blob/main/RoleDO.java) (ALL, DEPT_AND_CHILD, DEPT, SELF, CUSTOM).
- **Context Population**: `UserContext` and `DefaultDataPermissionUserDataProvider` supply user ID, department ID, and role scopes to the interceptor.
- **SQL Rewriting**: The MyBatis interceptor processes `@DataPermission` annotations and injects OR-combined WHERE clauses based on the aggregated role scopes.
- **Hierarchy Support**: The `ancestors` column in [`DeptDO.java`](https://github.com/continew-org/continew-admin/blob/main/DeptDO.java) enables the `DEPT_AND_CHILD` scope 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`](https://github.com/continew-org/continew-admin/blob/main/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.