Express.js: Phân quyền theo vai trò với package node_acl
Bài đăng này đã không được cập nhật trong 3 năm
Hầu hết các ứng dụng web đều sử dụng một bộ các role để cho phép người dùng được truy cập vào tài nguyên mà họ được phép truy câp. Chúng ta sẽ đi tìm hiểu một chút về vấn đề này và cùng xây dựng một ví dụ nhỏ.
Access Control List(ACL)
Là một "tài liệu" mô tả việc phân quyền cho các user trong một hệ thống, thường được biểu diễn dưới dạng một bảng các quyền mà user có thể có:
User | Read | Write | Publish |
---|---|---|---|
hoangdv | 1 | 1 | 1 |
minhnt | 1 | 1 | 0 |
jack | 1 | 0 | 0 |
Như trong bảng này chúng ta có thể thấy mỗi người là một hàng và có các quyền cụ thể gán cho từng người. Khi người dùng thực hiện một hành động, hàng của người dùng và cột hành động tương ứng sẽ được kiểm tra để xác định người dùng có quyền truy cập hay không.
Role Based Access Control(RBAC)
Là một một phương pháp kiểm soát truy cập trong các ứng dụng, khi đó người dùng sẽ được phân các vai trò và các vai trò sẽ xác định các quyền truy cập của họ. Loại này thường được mô tả dạng cây (tree) hoặc biểu đồ để biểu diễn sự kế thừa các quyền try cập. Với bảng ACL ở trên biểu đồ RBAC tương ứng có thể là: Ở phương pháp này cho phép các vai trò được thừa hưởng các quyền từ các vai trò khác từ đó ta có thể dễ dàng thêm các đặc quyền mới vào toàn bộ hệ thống các quyền truy cập. Bằng việc tách biệt người dùng thành các thành phần được xác định trước, chúng ta sẽ dễ dàng mô hình hóa việc bảo mật cho ứng dụng.
Coding
Chúng ta đã có hiểu biết cơ bản về mô hình RBAC - Logic của mô hình khá đơn giản: Chúng ta sẽ xác định các vai trò và mỗi vai trò có các đặc quyền tương ứng. Khi kiểm tra truy cập bạn kiểm tra vai trò đó và kiểm tra nó có quyền đó không. Chúng ta sẽ lấy mô hình dưới đây để làm ví dụ:
Name | Read | Write | Publish |
---|---|---|---|
manager | 1 | 1 | 1 |
writer | 1 | 1 | 0 |
guest | 1 | 0 | 0 |
Name | Role |
---|---|
hoangdv | manager |
minhnt | writer |
jack | guest |
Chúng ta sẽ dùng package node_acl để hiện thực hóa mô hình phân quyền ở trên.
Khởi tạo đối tượng acl
Package hỗ trợ backend bằng: memory, redis, mongodb (ngoài ra còn một số backend của các bên thứ 3 tự custom)
Ở ví dụ này chúng ta sẽ sử dụng memoryBackend
, đồng nghĩa với việc nếu ứng dụng của bạn bị khởi động lại thì toàn bộ thông tin phân quyền sẽ bị mất.
let acl = new node_acl(new node_acl.memoryBackend(), {
debug: (msg) => {
console.log('-DEBUG-', msg);
}
});
Định nghĩa các quyền truy cập cho các route của express app
Phương thức allow
cho phép tạo ra các vai trò và quyền truy cập cho vai trò tương ứng.
resources
là tài nguyên được phân quyền
permissions
là các quyền được áp dụng lên tài nguyên tương ứng. Là một string
hoặc [string]
. Các permisstion mặc định sẽ là http verb
(req.method.toLowerCase()).
acl.allow([
{
roles: 'manager',
allows: [
{
resources: '/posts/publish',
permissions: '*'
}
]
},
{
roles: 'writer',
allows: [
{
resources: '/posts',
permissions: 'post'
}
]
},
{
roles: 'guest',
allows: [
{
resources: '/posts',
permissions: 'get'
}
]
}
]);
Các vai trò được kế thừa quyền từ các vai trò khác
Như trong ví dụ chúng ta đã mô tả: manager
sẽ có quyền của mình và toàn bộ quyền của các nhóm khác, writer
thì có quyền của mình và quyền của vai trò guest
acl.addRoleParents('writer', 'guest');
acl.addRoleParents('manager', 'writer');
Phân quyền cho người dùng vào các vai trò
Chúng ta sẽ sử dụng định danh của một người dùng (vd: userId) để chỉ định một người dùng được phân vào nhóm nào. Thông tin này sẽ được Backend
của package acl
lưu lại.
// grant "manager" role
acl.addUserRoles("hoangdv", "manager");
// grant "writer" role
acl.addUserRoles("minhnt", "writer");
// grant "guest" role
acl.addUserRoles("jack", "guest");
Middleware cho route các tài nguyên ứng
Sau khi đã định nghĩa các vai trò và thêm người dùng vào các vai trò chúng ta sẽ thêm middleware cho các route của express app để thực hiện việc phân quyền.
Package có sắn một phương thức middleware: (numPathComponents?: number, userId?: Value | GetUserId, actions?: strings) => express.RequestHandler;
hỗ trợ việc kiểm tra người dùng có quyền truy cập tài nguyên tương ứng không. Phương thức acl.middleware
có 3 tham số không bắt buộc:
numPathComponents
: Số các phần trongoriginalUrl
sẽ được lấy để kiểm tra quyền truy cậpurl = req.originalUrl.split('?')[0]; if(!numPathComponents){ resource = url; }else{ resource = url.split('/').slice(0,numPathComponents+1).join('/'); }
userId
: là một string, number hoặc một function trả lại thông tin định danh của user đã dùng để cấu hình ở bướcPhân quyền cho người dùng vào các vai trò
. Nếu chúng truyền vào một function, function này sẽ nhận vào 2 tham sốreq, res
của express request, chú ý hàm này phải là một hàm đồng bộ trả về giá trịnh định danh của người dùng. Nếu truyền tham số này một giá trị falsy thì các giá trị mặc định sẽ được lấy làreq.session.userId || req.user.id
actions
: Quyền truy cập, mặc định sẽ làreq.method.toLowerCase()
Chúng ta có thểcustom
lại middleware cho việc này, khi đó chúng ta sẽ sử dụng phương thứcisAllowed: (userId: Value, resources: strings, permissions: strings, cb?: AllowedCallback) => Promise<boolean>;
để kiểm tra quyền truy cập của người dùng trên tài nguyên tương ứng. Với ví dụ của chúng ta, chúng ta sẽ có các route:
// Only for guests and higher
app.get('/posts', acl.middleware(1, getUserId), (req, res) => {
res.send('Read post!');
});
// Only for writer and higher
app.post('/posts', acl.middleware(1, getUserId), (req, res) => {
res.send('Post created!');
});
// Only for manager
app.post('/posts/publish', acl.middleware(0, getUserId), (req, res) => {
res.send('Post published!');
});
function getUserId(req) {
return req.query.uid; // (yaoming) just for fun
}
Toàn bộ file ví dụ:
const express = require('express');
const node_acl = require('acl');
let app = express();
app.set('port', process.env.PORT || 3000);
app.use((err, req, res, next) => {
if (!err) return next();
req.status(403).json({ message: err.msg, error: err.errorCode });
});
let acl = new node_acl(new node_acl.memoryBackend(), {
debug: (msg) => {
console.log('-DEBUG-', msg);
}
});
// This creates a set of roles which have permissions on
// different resources.
acl.allow([
{
roles: 'trumcuoi',
allows: [
{
resources: '/allow',
permissions: '*'
},
{
resources: '/disallow',
permissions: '*'
},
]
},
{
roles: 'manager',
allows: [
{
resources: '/posts/publish',
permissions: '*'
}
]
},
{
roles: 'writer',
allows: [
{
resources: '/posts',
permissions: 'post'
}
]
}, {
roles: 'guest',
allows: [
{
resources: '/posts',
permissions: 'get'
}
]
}
]);
// Inherit roles
// Every writer is allowed to do what guests do
// Every manager is allowed to do what writer do
acl.addRoleParents('writer', 'guest');
acl.addRoleParents('manager', 'writer');
initRoutes();
function initRoutes() {
// Defining routes ( resources )
// Simple overview of granted permissions
app.get('/info', (req, res) => {
acl.allowedPermissions(getUserId(req), ['/posts', '/posts/publish', '/allow/*', '/disallow/*'], (err, permission) => {
res.json(permission);
});
});
app.get('/user', (req, res) => {
acl.userRoles(getUserId(req), (err, roles) => {
res.json(roles);
})
});
// Only for guests and higher
app.get('/posts', acl.middleware(1, getUserId), (req, res) => {
res.send('Read post!');
});
// Only for writer and higher
app.post('/posts', acl.middleware(1, getUserId), (req, res) => {
res.send('Post created!');
});
// Only for manager
app.post('/posts/publish', acl.middleware(0, getUserId), (req, res) => {
res.send('Post published!');
});
// Setting a new role
app.get('/allow/:user/:role', acl.middleware(1, getUserId), (req, res) => {
acl.addUserRoles(req.params.user, req.params.role);
res.send(req.params.user + ' is a ' + req.params.role);
});
// Unsetting a role
app.get('/disallow/:user/:role', acl.middleware(1, getUserId), (req, res) => {
acl.removeUserRoles(req.params.user, req.params.role);
res.send(req.params.user + ' is not a ' + req.params.role + ' anymore.');
});
}
// grant "trumcuoi" role for default user
acl.addUserRoles("boss", "trumcuoi");
// grant "manager" role
acl.addUserRoles("hoangdv", "manager");
// grant "writer" role
acl.addUserRoles("minhnt", "writer");
// grant "guest" role
acl.addUserRoles("jack", "guest");
// Provide logic for getting the logged-in user
// This is a job for your authentication layer
function getUserId(req) {
return req.query.uid; // (yaoming) just for fun
}
app.listen(app.get('port'), () => {
console.log('ACL example listening on port ' + app.get('port'));
});
Khi chúng ta chạy ứng dụng và thực hiện truy cập:
GET /posts?uid=jack
=> ok
GET /posts?uid=minhnt
=> ok
GET /posts?uid=hoangdv
=> ok
POST /posts?uid=jack
=> fail
POST /posts?uid=minhnt
=> ok
POST /posts?uid=hoangdv
=> ok
POST /posts/publish?uid=jack
=> fail
POST /posts/publish?uid=minhnt
=> fail
POST /posts/publish?uid=hoangdv
=> ok
Kết luận
Hy vọng bài viết sẽ giúp mọi người có thêm các từ khóa và các ý tưởng để xây dựng các ý tưởng cho mình.
All rights reserved