RBAC in practice

 

Author: Nguyễn Phúc Vinh

Published date: 17/07/2022

Lời mở đầu

Bài viết này dựa trên kinh nghiệm cá nhân và tham khảo các nguồn textbook + paper + documents. Một số hình ảnh mình lấy sẵn trên internet. Bạn đọc góp ý bằng cách tạo issue trên github repo của mình nhé.

Content:

  1. Nhắc lại khái niệm Role-based Access Control (RBAC)
  2. Một số ứng dụng thực tế
    1. Trong DBMS
    2. Trong các hệ thống lớn
  3. Một ví dụ về implement RBAC sử dụng ExpressJS + MySQL (TypeScript)
    1. Technical Stack & Tools
    2. Cấu trúc thư mục
    3. Khởi tạo project
    4. Khởi tạo database
    5. Kết nối ứng dụng với database
    6. API Specification
    7. Hiện thực một mock API
    8. Middleware
    9. Hiện thực middleware
    10. Testing
    11. Khả năng mở rộng và tái cấu trúc code
    12. Cách chủ đề liên quan cần thực hiện để có được lớp bảo mật hoàn chỉnh
  4. Tham khảo

1 Nhắc lại khái niệm Role-based Access Control (RBAC)

RBAC hiện thực cách quản lý truy cập thông qua các vai trò (roles). Mỗi vai trò có các quyền hạn (permissions) với các tài nguyên/tập lệnh nhất định trong hệ thống (resources).

Để có overview rõ hơn về RBAC, các bạn có thể tìm hiểu thêm hoặc đọc bài viết sau của chúng tôi.

2 Một số ứng dụng trên thực tế

Phạm vi áp dụng của RBAC trải rộng từ hệ quản trị cơ sở dữ liệu (DBMS), các ứng dụng (applications) cho đến các môi trường (environments) làm việc và phát triển. Sau đây mình sẽ đi vào thực tế xem RBAC được thực hiện như nào nhé.

2.1 DBMS

Trong ngôn ngữ SQL, để đảm bảo tính bảo mật và điều khiển truy cập người dùng, ta sử dụng các câu lệnh DCL (data control language). Với 2 lệnh DCL cơ bản GRANT

REVOKE , chỉ có Admin cơ sở dữ liệu hoặc chủ sở hữu đối tượng cơ sở dữ liệu mới có thể cung cấp / xóa các đặc quyền / quyền trên một đối tượng cơ sở dữ liệu.

Privilages và Role trong SQL

Privilages (đặc quyền) được cấp cho một tài khoản sẽ quyết định tài khoản đó có thể thực thi những gì. Ta có một ví dụ:

GRANT SELECT ON tickfund.transaction to 'userA'@'localhost';

Khi admin thực thi câu lệnh trên, tài khoản kết nối tới DBMS thông qua local, có tên là userA sẽ có quyền thực thi các câu lệnh SELECT tới bảng transactions thuộc database có tên là tickfund. Vì không được cấp các quyền khác như UPDATE , DELETE , INSERT , userA sẽ không thể thực hiện các thao tác làm thay đổi dữ liệu trong bảng transaction. Khi muốn thu hồi đặc quyền từ user, ta sử dụng lệnh REVOKE. Một quyền không thể bị REVOKE khi chưa được cấp cho user bởi câu lệnh GRANT

Như vậy, ta có thể gắn trực tiếp các đặc quyền cho một user. Tuy nhiên, trong RBAC, các quyền sẽ được gắn với ROLE thay vì với user. Ta có thể tạo hoặc phá hủy một role như sau:

CREATE ROLE IF NOT EXISTS 'app_read', 'ticklab_dbadmin', 'app_write';
DROP ROLE IF EXISTS 'itmanager';
-- ...


Gắn các đặc quyền cho role:

GRANT ALL ON tickfund.* TO 'ticklab_dbadmin';
GRANT SELECT ON tickfund.* TO 'app_read';
GRANT INSERT, UPDATE, DELETE ON tickfund.* TO 'app_write'; 


Cấp role cho user:

GRANT ROLE TO 'userA'@'%';

Hầu hết các RDBMS hiện nay đều sử dụng khái niệm role, và các privilage được gắn cho role. RBAC trong RDBMS có thể kết hợp với các kỹ thuật điều khiển truy cập khác như Discrete Access Control, Fine-Grained Access Control …

2.2 Trong các hệ thống lớn

Tại phần này, ta sẽ có được một bức tranh lớn hơn khi áp dụng RBAC. Để hình dung được rõ hơn sự khác nhau giữa RBAC trong các hệ thống microservices với RBAC dưới tầng DBMS, ta có một bài toán minh họa.

Cho một hệ thống gồm các tài nguyên như sau: 2 Domain Controller, File Server, App Servers, Database Servers, Application, Related Servers, Network Devices, Log, Aggregator. Với các tài nguyên như vậy, ta có 3 chính sách (policy) được áp dụng như sau.

Policy Resources
Policy 1 Domain Controller 1, Domain Controller 2, File Server
Policy 2 App Server, Database Server, Application
Policy 3 Related Servers, Network Devices, Log Aggregator

2.2.1 Policy 1: Quyền truy cập máy chủ cơ sở hạ tầng cho các vai trò khác nhau

Các access level:

  • Admin access tới các server
  • RDP access tới các server
  • Share access tới các server

Quyền hạn được xác định cho các vai trò sau:

  • System Engineer
  • Network Engineer
  • DBA
  • IT Manager
  • End Users
  • External Users

Đặc tả chính sách:

  • External users được xem như chưa xác thực và không có quyền access tới bất
    kỳ resource nào
  • IT Manager không được đồng thời có quyền Admin và RDP access. Điều này
    đảm bảo phân tách nhiệm vụ của một người trong công ty
  • Network Engineers và DBA không có quyền Admin hay RDP access tới bất kỳ
    server nào
  • Chỉ System Engineers mới có quyền Admin và RDP Access tới tất cả các
    server
  • Tất cả user còn lại, trừ external user, có quyền share access

2.2.2 Policy 2: Application và Database Servers

Các level access:

  • Admin Access tới application servers
  • Admin Access tới database servers
  • Database Admin access (DBA)
  • Production Application Access
  • Production Data Access
  • Test Application Access
  • Test/De-identified Data access
  • Developer
  • Tester
  • Server Admin
  • DBA
  • Application Admin Users
  • Application User
  • Các user khác

Đặc tả chính sách

  • Production Servers và Test Servers:
    • Developers và Testers không có bất kỳ quyền truy cập nào
    • Server Admins có tất cả quyền Admin access ttới Application và Database
      Servers
    • DBA chỉ có quyền Admin access tới Database servers
    • DBA có quyền Database Access
    • Không người dùng nào có quyền access tới các server
  • Production Application:
    • Developers, Testers, Server Admins và DBA không có bất kỳ quyền truy
      cập nào
    • Các application admin users chỉ có quyền truy cập tới các giao diện admin
    • Các application end users khác chỉ có quyền truy cập tới các giao diện non-
      admins
  • Test Application
    • Developers và Testers có toàn quyền truy cập
    • Server Admins và DBA không có bất kỳ quyền truy cập nào
    • Các application admin users chỉ có quyền truy cập tới các giao diện admin
    • Các application end users khác chỉ có quyền truy cập tới các giao diện non-
      admins
  • Prod PHI/PII Application Data
    • Chỉ DBA và các application users mới có quyền truy cập
  • De-Identified Application Data
    • Testers, Developers, DBA, và các application users có quyền truy cập.
    • Server Admins và các user khác không có quyền truy cập

2.2.3 Policy 3: Log Access

Các level access:

  • Admin access
  • Log Review
  • Log Access

Quyền hạn được xác định cho các vai trò sau:

  • Server Engineer
  • Network Engineer
  • Security Engineer

Đặc tả chính sách:

  • Server access chỉ được cấp cho Server Engineers. Server Engineer và Securiy
    Engineer có quyền log review access trên các server.
  • Network device access chỉ được cấp cho Network Engineers
  • Network Engineer và Security Engineer có quyền log review access trên các
    network devices
  • Log Aggregator access chỉ được cấp cho Security Engineer

Tóm tắt các vai trò (roles):

Policy Roles
Policy 1 System Admin, Network Engineer, DBA, IT Manager, End Users, External
Users
Policy 2 Developer, Tester, Server Admins, DBA, App ADMIN, End Users, Other Users
Policy 3 Server Engineer, Network Engineer, Security Engineer

Tóm tắt các action

Policy Roles
Policy 1 Admin Access, Share Access, RDP Access
Policy 2 Admin Access, DB Access, App Access, AppAdmin Access, Data Access
Policy 3 Admin Access, Log Review

Mô hình phân quyền trên được thiết kế trong bài báo RBAC for Healthcare-Infrastructure and data storage. Khác với RBAC trong các DBMS, trong hệ thống lớn ta có những dạng user, role, resource và action cần phải tự định nghĩa và mô hình hóa. Hiện tại, để quản lý access control cho các hệ thống, có rất nhiều tool support như NGINX Instance Manager, NGINX Controller, Kubernetes …

3 Implement RBAC tại tầng ứng dụng

Như vậy ta đã có được góc nhìn về RBAC trong các DBMS và trong một hệ thống microservices. Sau đây mình sẽ hiện thực một bài toán RBAC tại tầng ứng dụng.

Đợi chút, tại sao phải làm ở tầng ứng dụng, trong khi hệ thống lớn đã có phân quyền ?

Để trả lời cho câu hỏi này, mình sẽ ví ứng dụng như một service trong hệ thống lớn. Khi hệ thống lớn điều khiển truy cập, mục đích chính là ngăn chặn các cuộc tấn công bảo mật, và hạn chế truy cập theo vai trò nghiệp vụ. Còn phân quyền tại tầng ứng dụng, mục đích chủ yếu là phân chia quyền của các user. Quay lại phần 2, đối tượng phân quyền tại tầng ứng dụng chính là các Admin User, User và External User.

Còn phân quyền tại DBMS ? Mỗi một ứng dụng đều sử dụng các DBMS trong hệ thống, vì vậy các ứng dụng chính là user của DBMS (không phải user dạng Nguyễn Văn A). Thử tưởng tượng 1 developer được cấp user root của DBMS, và code một dòng truy vấn “DROP DATABASE” …

Quay lại vấn đề chính, để minh họa việc hiện thực RBAC, mình đưa ra một bài toán như sau: giả sử ta cần xây dựng ứng dụng phục vụ nội bộ cho một công ty. Trong công ty có các bộ phận:

  • Marketing
  • Finance
  • Human Resources

Mỗi nhân viên trong công ty đều thuộc một trong ba bộ phận trên. Mô tả quyền truy cập tài nguyên hệ thống:

Bộ phận Tài nguyên được phép truy cập
Marketing HubSpot, Google Analytics, Facebook Ads, Google Ads
Finance Xero, ADP
Human resources Lever, BambooHR

Có nhiều kiểu access level, và tùy vào tài nguyên sẽ có tập các access level khác nhau. Trong ví dụ này, để đơn giản hóa, mình sẽ chỉ sử dụng hai access level chính là access (read-only) và sudo (toàn quyền với tài nguyên, từ này mình mượn của hệ điều hành Linux). Dưới đây là mô tả level truy cập của các tài nguyên trong server.

Tài nguyên Level truy cập
HubSpot access, sudo
Google Analytics access
Facebook Ads access, sudo
Google Ads access
Xero access, sudo
ADP access
Lever access
BambooHR access, sudo

Dưới đây là thiết kế database (minimal) cho bài toán RBAC đặt ra:

Sau đây mình sẽ xây dựng 1 ứng dụng RESTful API để hiện thực hóa bài toán phân quyền trên. Để tập trung vào phần authorization, những phần như đăng nhập, mình sẽ tối giản hóa (chỉ sử dụng user ID, không cần mật khẩu), lưu trực tiếp role của user vào cookie mà không sử dụng JSON Web Token để encrypt. Các API sẽ được mock bằng cách đơn giản trả về chuỗi string như “Lever Resources” hoặc “Bamboo Resources” mà không hiện thực chi tiết.
OK let’s go !

3.1 Technical Stack & Tools

Trong phần implement, mình sẽ sử dụng NodeJS + Express + MySQL và viết bằng TypeScript. Để test API, mình sẽ sử dụng phần mềm Postman. Những công cụ cần cài đặt đầu tiên:

Tech Version
Node 16.13.1
MySQL Latest
npm 8.1.2

Trước khi bắt đầu, hãy cài đặt các tool cần thiết.

3.2 Cấu trúc thư mục

Bạn có thể tham khảo repository tại đây: https://github.com/phucvinh57/RBAC-Example

3.3 Khởi tạo project

Tạo một thư mục mới có tên example

mkdir example

Khởi tạo project (nhấn phím enter cho tới khi ok để sử dụng cài đặt mặc định):

npm init

Cài đặt TypeScript ở development mode:

npm i -D typescript

Set up TypeScript cho project:

npx tsc --init

Sau khi file tsconfig.json được tạo, copy & paste config bên dưới:

{

"compilerOptions": {
    /* Visit https://aka.ms/tsconfig to read more about this file */
    /* Language and Environment */
    "target": "es2016",
    /* Modules */
    "module": "commonjs",
    "rootDir": "./",
    "outDir": "./dist",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    /* Type Checking */
    "strict": true,
    "strictPropertyInitialization": true,
  }
}

Cài đặt các dependency cần thiết:

npm i express cookie-parser dotenv mysql2

Cài đặt dev dependency:

npm i -D @types/cookie-parser @types/express @types/node ts-node-dev

Chỉnh sửa file package.json như đoạn code bên dưới:

{
"name": "example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {

"build": "npx tsc",
"start:prod": "node dist/index.js",
"start:dev": "npx ts-node-dev index.ts",
"test": "echo \"Error: no test specified\" && exit 1"

},
"author": "",
"license": "ISC",
"dependencies": {

"cookie-parser": "^1.4.6",
"dotenv": "^16.0.1",
"express": "^4.18.1",
"mysql2": "^2.3.3"

},
"devDependencies": {

"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.13",
"@types/node": "^16.11.43",
"ts-node-dev": "^2.0.0",
"typescript": "^4.7.4"
}
}

Trong thư mục root, tạo một file tên là index.ts có nội dung như sau:


import express, { Express, Request, Response } from "express"; 
const PORT = 4000;
const app: Express = express();

app.get('/', function(req: Request, res: Response) {
   res.send("Hello world !!")
})

app.listen(PORT, () => {
   console.log(`Listening on port \({PORT}`)
})

 

Thử gõ lệnh npm run start:dev và nhập http://localhost:4000 trên thanh url của browser. Bạn sẽ thấy kết quả như sau:

Như vậy là ta đã khởi tạo xong project.

Lệnh npm run start:dev sử dụng để host app backend trong development mode, mỗi khi bạn thay đổi code, app sẽ tự động hot reloading. Để build ra production (transpile các file TS sang JS), sử dụng lệnh npm run build . Để chạy app sau khi build, gõ npm run start:prod . Bạn có thể xem các lệnh này trong file package.json.

3.4 Khởi tạo database

Bước này chỉ thực hiện được khi đã cài đặt xong MySQL trong máy. Ta sẽ không sử dụng tài khoản root để kết nối database vì lý do bảo mật, thay vào đó tạo một user mới và giới hạn các quyền thực thi truy vấn.
Trước tiên, bạn sẽ làm công việc của DBA, cụ thể đăng nhập MySQL bằng tài khoản root và thực hiện khởi tạo schema của database:

-- Use as root
CREATE DATABASE IF NOT EXISTS rbac_example;
USE rbac_example;

CREATE TABLE IF NOT EXISTS `role` (

id INT PRIMARY KEY AUTO_INCREMENT,

`name` VARCHAR(255) UNIQUE NOT NULL

);

CREATE TABLE IF NOT EXISTS `user` (

id INT PRIMARY KEY AUTO_INCREMENT,

`name` VARCHAR(255) UNIQUE NOT NULL,

role_id INT DEFAULT NULL,

FOREIGN KEY (role_id) REFERENCES `role`(id) ON DELETE SET NULL

);
CREATE TABLE IF NOT EXISTS `resource` (

id INT PRIMARY KEY AUTO_INCREMENT,

`name` VARCHAR(255) UNIQUE NOT NULL

);
CREATE TABLE IF NOT EXISTS `action` (

id INT PRIMARY KEY AUTO_INCREMENT,

`name` VARCHAR(255) UNIQUE NOT NULL

);
CREATE TABLE IF NOT EXISTS `permission` (

role_id INT,
resource_id INT,
action_id INT,
PRIMARY KEY (role_id, resource_id, action_id),
FOREIGN KEY (role_id) REFERENCES `role`(id) ON DELETE CASCADE,

FOREIGN KEY (resource_id) REFERENCES `resource`(id) ON DELETE CASCADE,

FOREIGN KEY (action_id) REFERENCES `action`(id) ON DELETE CASCADE

);

Sau khi khởi tạo xong schema cho database, tạo một user cho việc kết nối DB cấp quyền cho user đó:

-- Creates the user
-- Note that this password is an example, replace by your own one
CREATE USER 'rbac_example_dbuser'@'%' IDENTIFIED BY 'yourPassword#1';

-- Gives all privileges to the new user on the newly created database
GRANT ALL ON rbac_example.* to 'rbac_example_dbuser'@'%';

Để có data sử dụng, mình có define sẵn 1 số data. Bạn cần insert vào database vừa tạo bằng các câu truy vấn sau:


INSERT INTO `role`(id, `name`)
VALUES
   (1, 'hr'),
   (2, 'marketing'),
   (3, 'finance');

INSERT INTO `user`(`name`, role_id)
VALUES
   ('Bob', 1),
   ('Alex', 2),
   ('William', 3);

INSERT INTO `resource`(id, `name`)
VALUES
   (1, 'HubSpot'),
   (2, 'Google Analytics'),
   (3, 'Facebook Ads'),
   (4, 'Google Ads'),
   (5, 'Xero'),
   (6, 'ADP'),
   (7, 'Lever'),
   (8, 'BambooHR');

INSERT INTO `action`(id, `name`)
VALUES
   (1, 'access'),
   (2, 'sudo');

INSERT INTO permission(role_id, action_id, resource_id)
VALUES
   -- marketing resources access
   (2, 1, 1),
   (2, 1, 2),
   (2, 1, 3),
  (2, 1, 4),
   -- finance access
   (3, 1, 5),
   (3, 1, 6),
   -- human resources access
   (1, 1, 7),
   (1, 1, 8);

Kiểm tra kết quả:

SHOW TABLES;

SELECT * FROM `user`;


 

 

3.5 Kết nối ứng dụng với database

Để kết nốt tới DB, ta cần các package mysql2, dotenv đã cài đặt trong phần 3.3. Tạo một file tên là .env trong thư mục gốc và điền các thông tin kết nối tới database. Đây là file cần được bảo mật, vì thế trước khi push lên Github mình đã ignore.


DB_HOST="localhost"
DB_PORT=3306 # Default port of MySQL, must modify if your MySQL run on another.
DB_USER="rbac_example_dbuser"
DB_PASSWORD="yourPassword#1" # Replace your password here
DB="rbac_example"

Trong thư mục models, tạo file index.ts có nội dung như sau:


import mysql, { Connection, QueryError } from "mysql2";
import { config } from 'dotenv';
import util from 'util';

config()

if(!process.env.DB) {
   console.log("Database not selected");
   process.exit(0)
}

const connection: Connection = mysql.createConnection({
   host: process.env.DB_HOST,
   port: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306,
   user: process.env.DB_USER,
   password: process.env.DB_PASSWORD,
   database: process.env.DB
})

export const connectToDb = () => connection.connect((err: QueryError | null) => {
   if (err) console.log(err)
   else console.log("Database connected successfully !")
})

export default connection;

Thay đổi file entry index.ts:

import express, { Express } from "express";
import cookieParser from "cookie-parser";
import { connectToDb } from "./models";

const PORT = 4000;

const app: Express = express();

connectToDb()

app.use(cookieParser())

app.use(express.json())

app.listen(PORT, () => {
   console.log(`Listening on port \){PORT}`)
})

Kết quả sau khi kết nối database thành công:

3.6 API Specification

Sau khi set up mọi thứ Ok, giờ mới là phần hiện thực. API sẽ được thiết kế như sau:

Path Method Chức năng Mô tả
/login POST Đăng nhập - Thành công: User ID,
user name, role của
user và các permission 
- Thất bại: User Not Found
/logout POST Đăng xuất - Một message báo đăng
xuất thành công.
- Sẽ trả lỗi khi chưa login mà
đăng xuất
/hr/lever GET Truy cập resource Lever Mock String
/hr/bamboo-hr GET

Truy cập resource

BambooHR

Mock String
/hr/ADP GET Truy cập resource ADP Mock String
/finance/xero GET Truy cập resource Xero Mock String
/marketing/hubspot GET Truy cập resource HubSpot Mock String
/marketing/google-analytics GET Truy cập resource Google Analytics Mock String
/marketing/google-ads GET Truy cập resource Google Ads Mock String
/marketing/facebook-ads GET Truy cập resource Facebook Ads Mock String

 

Các router sẽ làm nhiệm vụ điều hướng request tới các controller để xử lý dựa trên URL path. Các router bao gồm:

  • Auth Router: điều hướng /login và /logout

  • HR Router: điều hướng các path có dạng /hr/*

  • Finance Router: điều hướng các path có dạng /finance/*

  • Marketing Router: điều hướng các path có dạng /marketing/*

3.7 Hiện thực một mock API

Để có được một mock API, ta cần một controller và một router. Trong thư mục routes, tạo một file tên là hr.route.ts có nội dung như sau:

Đầu tiên ta sẽ tạo controller trước. Trong thư mục controllers, tạo một file tên là hr.controller.ts có nội dung như sau:


import { Request, Response } from "express"

const hrController = {
   lever: function(req: Request, res: Response) { res.send("LEVER Resources")},
   bambooHR: function(req: Request, res: Response) { res.send("BambooHR Resources")}
}

export default hrController;

 

Hai controller bambooHR và lever sẽ nhận request và trả về mock string đơn giản.

Tiếp tục tạo router để nhận điều hướng request tới controller vừa tạo. Trong thư mục routes , thêm 1 file tên là hr.route.ts :

import { Router } from "express";
import hrController from "../controllers/hr.controller";
import hrRoleChecker from "../middlewares/hr.middleware";

const hrRouter: Router = Router()

hrRouter.get('/lever',hrController.lever);

hrRouter.get('/bamboo-hr', hrController.bambooHR)

export default hrRouter;

Tạo mới một file tên là index.ts trong thư mục routes, import hrRouter như sau:


import { Express } from "express";
import hrRouter from "./hr.route";

export default function route(app: Express) {
   app.use('/hr', hrRouter)
}

 

Thay đổi file index.ts trong thư mục gốc:


import express, { Express } from "express";
import cookieParser from "cookie-parser";
import route from "./routes";
import { connectToDb } from "./models";

const PORT = 4000;
const app: Express = express();
connectToDb()

app.use(cookieParser())
app.use(express.json())
route(app)

app.listen(PORT, () => {
   console.log(`Listening on port ${PORT}`)
})

Restart lại app backend (không cần nếu sử dụng npm run start:dev) và nhập các url http://localhost:4000/hr/lever và http://localhost:4000/hr/bamboo-hr để kiểm tra kết quả.

Như vậy chúng ta đã hiện thực xong 2 mock API là /hr/lever và /hr/bamboo-hr. Phần còn lại các bạn làm tương tự để đủ các API đã define.

3.8 Middleware

Sau khi có API, ta có thể thoải mái truy cập vào tài nguyên hệ thống mà không bị kiểm soát. Vì vậy, trước khi để request chạm được tới controller, ta cần thêm các middleware ở giữa để chặn request không hợp lệ. Để đi tiếp, trước hết mình sẽ giới thiệu qua flow của token-based authentication.

Trước hết, để gọi được các API có lớp bảo mật, user phải đăng nhập. Khi đó server sẽ tạo ra một token và gửi về kèm trong response của user. Lần gọi API kế tiếp, user sẽ sử dụng token vừa nhận để gửi request. Mỗi lần nhận request, server sẽ kiểm tra token có hợp lệ hay không, rồi mới đưa request của user tới lớp Controller.

Trong phần implement này, để đơn giản hóa mình sẽ không sử dụng token, mà đơn giản chỉ lưu một số thông tin cần thiết của user trong cookies, để phục vụ cho các request sau. Việc thực hiện điều khiển truy cập RBAC sẽ được hiện thực tại lớp middleware.

Như vậy để thực hiện phần middleware, bắt buộc phải có chức năng login. Mình sẽ để các bạn tự khám phá trong source code, mọi thắc mắc vui lòng liên hệ bằng cách tạo issue trong link github repo của mình.

3.9 Hiện thực middleware

Sau khi implement chức năng login, mình có được kết quả như sau:

  • Đăng nhập thành công, user sẽ có userId, role, và list các permission được lưu trong cookies.

  • Khi đăng xuất, server sẽ xóa cookies của user. User cần đăng nhập lại để có được cookies mới, phục vụ cho việc author

Trong thư mục middlewares , tạo một file tên là hr.middleware.ts. Mình sẽ hiện thực role checker cho việc truy cập resource tại đây.


import { Request, Response, NextFunction } from "express";
import { FORBIDDEN_CODE } from "../constants";
import Exception from "../exceptions";

// Get User's permission from cookies
function getPermission(req: Request) {
    const userPermission: {
       resource: string | null,
       action: string | null
    }[] = req.cookies['permission'];
   return userPermission
}

const hrRoleChecker = {
   lever: function (req: Request, res: Response, next: NextFunction) {
      const userRole: string | null = req.cookies['role'];
      const userPermission = getPermission(req);
      
      //If user exists but not be granted any roles, deny access
      if (!userRole) {
         res.status(FORBIDDEN_CODE).json(new Exception("Permission denied"))
         return
      }

      // Check if user permission satisfies the guard
      if(!userPermission.some(permission =>
           permission.action?.toLowerCase() == 'access' // Can be replace
           && permission.resource?.toLowerCase() == 'lever' // Can be replace
       )) {
          res.status(FORBIDDEN_CODE).json(new Exception("Permission denied"))
          return
       }
       
       // Move to next controller
       next()
   },
   bambooHR: function (req: Request, res: Response, next: NextFunction) {
      const userRole: string | null = req.cookies['role'];
      if (!userRole) {
         res.status(FORBIDDEN_CODE).json(new Exception("Permission denied"))
         return
      }

      const userPermission = getPermission(req);
      if(!userPermission.some(permission =>
          permission.action?.toLowerCase() == 'sudo' // Can be replace
          && permission.resource?.toLowerCase() == 'bamboohr' // Can be replace
      )) {
         res.status(FORBIDDEN_CODE).json(new Exception("Permission denied"))
         return
      }
      next()
   },
}

export default hrRoleChecker

 

Chèn middleware trước hrController (trong file routes/hr.route.ts):

hrRouter.get('/lever', hrRoleChecker.lever ,hrController.lever);
hrRouter.get('/bamboo-hr', hrRoleChecker.bambooHR, hrController.bambooHR)

Với các hiện thực như trên, chỉ khi nào user có access level là access với tài nguyên Lever mới gọi được API GET /hr/lever , và chỉ user có access level là sudo với tài nguyên BambooHR mớii gọi được GET /hr/bamboo-hr.

3.10 Testing

Sử dụng Postman để tạo request đăng nhập:

Lúc này user có level access tới tài nguyên Lever là access. Tạo GET request /hr/lever: 

Kết quả trả về là mock string (pass test)
Tiếp tục gửi GET request tới /hr/bamboo-hr:

Kết quả là permission denied, vì user cần level access là sudo (pass test).
Hiện tại mình chỉ hiện thực middleware cho API có dạng /hr/*. Với các mock API còn lại, các bạn có thể tự hiện thực

3.11 Khả năng mở rộng và tái cấu trúc code.

Trong phần middleware chặn request tới tài nguyên BambooHR, mình có code như sau:

const userRole: string | null = req.cookies['role'];
const userPermission = getPermission(req);

// If user exists but not be granted any roles, deny access
if (!userRole) {

res.status(FORBIDDEN_CODE).json(new Exception("Permission denied"))

return

}

Phần check trường hợp nếu user tồn tại, đã đăng nhập nhưng không có role, có thể tách thành middleware.

Dưới đây là một đoạn code khác:


const userPermission = getPermission(req);
if(!userPermission.some(permission =>
   permission.action?.toLowerCase() == 'sudo' // Can be replace
   && permission.resource?.toLowerCase() == 'bamboohr' // Can be replace
)) {
   res.status(FORBIDDEN_CODE).json(new Exception("Permission denied"))
   return
}

 

Hai dòng comment “Can be replace” của mình đánh dấu phép so sánh permission của user là hard code. Có thể thay thế bằng cách tạo một constant trong file constants/index.ts . Nhưng điều gì sẽ xảy ra, nếu nhu cầu người dùng cần policy này thay đổi ? Để dễ hình dung, mình đưa ra ví dụ admin user cần có 1 cái UI để thay đổi policy này. Thay vì cần access level là sudo , admin muốn chỉnh thành access . Lúc đó string “sudo” tại dòng comment thứ nhất cần được thay thế bởi một biến, mà biến này có giá trị đọc từ database và có thể thay đổi được bởi admin thông qua request.

Ngoài ra, admin muốn user cần tạo composite permission trong các trường hợp sau:

  • User cần cả hai access level mới truy cập được tài nguyên
  • Có một API cần truy cập tài nguyên trong hệ thống, cần có tất cả các permission liên quan mới gọi API được.
  • .....

Như vậy nên implement như thế nào, để user admin có thể chỉnh sửa các policy bằng giao diện ? Đây là câu hỏi mở mình sẽ để lại cho bạn đọc.

3.12 Các chủ đề liên quan cần thực hiện để có được lớp bảo mật hoàn chỉnh

  • Set up SSL cho server
  • Basic Authentication
  • JWT
  • Where to save token ?
  • Top 10 OSWAP

4 Tham khảo

[1] Elmasri, R., Navathe, S. (1989). Fundamentals of Database Systems, 30 (3), 1137-1139, Benjamin/Cummings.
[2] Ramesh Narasimman, Izzat Alsmadi. RBAC for Healthcare-Infrastructure and data storage. 1 - 2, 4 - 7
[3] Using RBAC with the App Security Add-On, https://docs.nginx.com/nginx-controller/app-delivery/security/tutorials/using-rbac-with-app-security/
[4] Role Based Access Control Good Practices,https://kubernetes.io/docs/concepts/security/rbac-good-practices/

 


Print   Email

Add comment

Related Articles

BIẾN ĐỔI FOURIER