Giao diện
Auth & Permission
Request pipeline
Mỗi HTTP request đi qua chuỗi middleware theo thứ tự:
setCurrentRole - xác định role
Middleware này chạy sau auth và trước acl:
- Đọc header
X-Roletừ request. - Load danh sách role của user (từ cache
roles:{userId}hoặc DB). - Áp dụng system role mode:
default→ dùng role từX-Rolehoặc roledefaultcủa user.allow-use-union→ nếuX-Role=__union__, merge tất cả role.only-use-union→ luôn merge tất cả role.
- Set
ctx.state.currentRolevàctx.state.currentRoles.
checkQueryPermission - lọc truy vấn
Trước khi query data, middleware merge ACL filter vào query:
Ví dụ: role member có strategy view:own → filter tự động thêm { createdById: '{{ ctx.state.currentUser.id }}' } vào mọi query.
checkChangesWithAssociation - kiểm tra mutation
Trước create/update, middleware kiểm tra:
- Field whitelist - chỉ cho phép sửa field mà role được grant.
- Association sanitization - lọc bỏ association data ngoài quyền.
checkAssociationOperate - kiểm tra thao tác association
Trước add, set, remove, toggle trên association, kiểm tra quyền trên resource đích.
withACLMeta - bổ sung meta vào response
Khi client gửi header X-With-ACL-Meta, response list và get được bổ sung:
json
{
"data": [...],
"meta": {
"allowedActions": {
"view": [1, 2, 3],
"update": [1, 2],
"destroy": [1]
}
}
}Client dùng meta này để ẩn/hiện nút Edit, Delete cho từng row.
Role hệ thống
root - superuser
- Ẩn khỏi danh sách role trên UI (
hidden: true). - Bypass tất cả kiểm tra quyền:
acl.allow('*', '*', (ctx) => currentRoles.includes('root')). - Snippet:
ui.*,pm,pm.*- truy cập toàn bộ UI và plugin settings. - Không thể gán role
rootcho user qua UI (rolesUsers.beforeSavechặn).
admin - quản trị viên
allowConfigure: true- truy cập trang cấu hình.allowNewMenu: true- tạo menu page mới.- Strategy:
create,view,update,destroy- toàn quyền CRUD trên mọi collection (theo strategy). - Snippet:
ui.*,pm,pm.*.
member - thành viên
default: true- user mới tự động nhận role này.allowNewMenu: true.- Strategy:
view:own- chỉ xem bản ghi do mình tạo. - Snippet:
!ui.*,!pm,!pm.*- phủ định → không truy cập UI config và plugin settings.
anonymous - khách (ẩn)
- Middleware
roles:listmặc định lọc bỏanonymoustrừ khi truyềnshowAnonymous. - Dùng cho request không xác thực (public pages).
Scope - phân quyền theo dữ liệu
Scope định nghĩa filter tự động áp dụng khi user truy cập data.
Scope mặc định
| Key | Tên | Filter | Ý nghĩa |
|---|---|---|---|
all | Tất cả | (trống) | Truy cập mọi bản ghi |
own | Của mình | { createdById: '{{ ctx.state.currentUser.id }}' } | Chỉ bản ghi do user tạo |
Custom scope
Admin có thể tạo scope tùy chỉnh cho từng collection:
json
{
"name": "Cùng phòng ban",
"resourceName": "orders",
"scope": {
"departmentId": "{{ ctx.state.currentUser.departmentId }}"
}
}Template {{ ctx.state.currentUser.* }} được thay thế bằng giá trị thực tại runtime.
Cách scope áp dụng
Ví dụ thực tế
403 khi không có quyền
typescript
// Role member chỉ có strategy 'view' → cố create sẽ bị từ chối
await db.getRepository('roles').update({
filterByTargetKey: 'member',
values: { strategy: { actions: ['view'] } },
});
const memberAgent = await app.agent().login(memberUser);
const response = await memberAgent.resource('posts').create({
values: { title: 'test' },
});
// response.statusCode === 403Field-level filtering - response tự lọc field
typescript
// Role chỉ được view field "title" (không "description")
await agent.resource('roles.resources', 'editor').create({
values: {
name: 'posts',
usingActionsConfig: true,
actions: [
{ name: 'create', fields: ['title', 'description'] },
{ name: 'view', fields: ['title'] },
],
},
});
await agent.resource('posts').create({
values: { title: 'Hello', description: 'World' },
});
const listResp = await editorAgent.resource('posts').list();
const post = listResp.body.data[0];
// post.id → có (PK luôn trả về)
// post.title → 'Hello'
// post.description → undefined (bị lọc bởi ACL)Scope template - chỉ xem bản ghi của mình
typescript
// Tạo scope "own" dùng template currentUser
const { body: { data: scope } } = await agent
.resource('dataSourcesRolesResourcesScopes')
.create({
values: {
name: 'own',
scope: { createdById: '{{ ctx.state.currentUser.id }}' },
},
});
// Gán scope vào action view
await agent.resource('roles.resources', 'editor').create({
values: {
name: 'posts',
usingActionsConfig: true,
actions: [
{ name: 'view', fields: ['title'], scope: scope.id },
],
},
});
// User chỉ thấy bản ghi do mình tạo
const listResp = await editorAgent.resource('posts').list();
// listResp.body.data chỉ chứa posts có createdById = editor.idACL meta - phân quyền UI từng row
typescript
const response = await agent
.set('X-With-ACL-Meta', true)
.resource('posts')
.list({});
const { meta } = response.body;
// meta.allowedActions.view = ['t1', 't2', 't3']
// meta.allowedActions.update = ['t1', 't2']
// meta.allowedActions.destroy = ['t3']
// Client dùng meta này ẩn/hiện nút Edit, Delete cho từng rowCache
Plugin cache thông tin role để tránh query DB mỗi request:
| Cache key | Nội dung | Invalidated khi |
|---|---|---|
roles:{userId} | Danh sách role name của user | rolesUsers thay đổi |
roles:{userId}:defaultRole | Role mặc định | rolesUsers thay đổi |
Events cache:del:roles cũng xóa cache role.
Bảo mật - lưu ý
- root có quyền tuyệt đối - hạn chế số user, audit đăng nhập.
rolesUsers.beforeSavechặn gán rolerootqua API quan hệ (chống leo thang quyền).- Middleware destroy so sánh
countgiữa filter user gửi và filter sau merge ACL - ngăn xóa vượt phạm vi. - Fixed params bảo vệ role/scope/collection hệ thống khỏi bị xóa.
- Snippet
pm.acl.roleschỉ nên gán cho admin tin cậy.