Trong quá trình làm việc hẳn mọi người từng nghe qua Authorization - hay phân quyền. Có nhiều mô hình phân quyền phổ biến gồm: ACL (Access Control Lists), RBAC (Role-Based Access Control), ABAC (Attribute-Based Access Control),… Hôm nay cùng mình thử tìm hiểu về RBAC và viết thử 1 dự án đơn giản nhé.
Vậy RBAC là gì?
RBAC là viết tắt của “Role-Based Access Control”, trong tiếng Việt có thể dịch là “Kiểm soát truy cập dựa trên vai trò”. RBAC xác định quyền truy cập dựa trên vai trò của người dùng thay vì quản lý từng người dùng riêng lẻ. Trong RBAC, người dùng được gán vào các vai trò khác nhau, và mỗi vai trò có các quyền truy cập tương ứng. Việc gán vai trò cho người dùng dựa trên công việc hoặc trách nhiệm của họ trong hệ thống.
RBAC giúp đơn giản hóa việc quản lý quyền truy cập bằng cách tập trung vào vai trò và gán quyền truy cập cho từng vai trò. Nó cung cấp sự linh hoạt và dễ dàng mở rộng hệ thống trong khi giảm thiểu việc quản lý quyền truy cập chi tiết cho từng người dùng riêng lẻ.
Ví dụ: hệ thống của TechMaster có các role: Admin, User, Trainer, Student,… Anh A muốn đăng kí học ở TechMaster, hệ thống sẽ cung cập cho anh A role Student. Từ đó, anh A có thể dùng các quyền mà role Student được cấp.
Ưu, nhược điểm của RBAC
Ưu điểm:
- Quản lý dễ dàng: RBAC giúp đơn giản hóa quản lý quyền truy cập bằng cách tập trung vào vai trò thay vì từng người dùng riêng lẻ. Việc gán vai trò cho người dùng dựa trên trách nhiệm hoặc công việc của họ trong hệ thống giúp quản lý dễ dàng hơn và giảm sự phức tạp của việc quản lý quyền truy cập chi tiết cho từng người dùng.
- Linh hoạt và mở rộng: RBAC cho phép hệ thống mở rộng và thay đổi mà không cần thay đổi cấu trúc quyền truy cập. Khi có một vai trò mới hoặc một người dùng mới, chỉ cần gán vai trò tương ứng và không cần phải tạo ra các quyền truy cập mới.
- Bảo mật cải thiện: RBAC giúp cải thiện bảo mật bằng cách hạn chế quyền truy cập chỉ cho những người dùng cần thiết để thực hiện công việc của họ. Bằng cách xác định và kiểm soát quyền truy cập theo vai trò, rủi ro an ninh và lỗi nhân viên có thể được giảm thiểu.
Nhược điểm: - Khó triển khai ban đầu: Xây dựng một hệ thống RBAC có thể đòi hỏi sự phân tích và thiết kế kỹ lưỡng. Điều này có thể tốn nhiều thời gian và nguồn lực để triển khai một cách chính xác, đặc biệt là đối với các hệ thống lớn và phức tạp.
- Quản lý vai trò phức tạp: Trong các hệ thống lớn có nhiều vai trò và quyền truy cập, quản lý và duy trì vai trò có thể trở nên phức tạp. Điều này đòi hỏi một quy trình quản lý chặt chẽ để đảm bảo tính nhất quán và hiệu quả của hệ thống.
- Thiếu linh hoạt trong xử lý ngoại lệ: RBAC không linh hoạt trong xử lý các tình huống ngoại lệ hoặc trường hợp đặc biệt mà yêu cầu phân quyền linh hoạt hơn. Trong những trường hợp này, cần có các phương pháp bổ sung để xử lý các yêu cầu truy cập đặc biệt.
- Khó khăn trong việc quản lý quyền truy cập động: RBAC không phù hợp khi cần xử lý các quyền truy cập động dựa trên các điều kiện thay đổi. Điều này đòi hỏi sự mở rộng của RBAC để bao gồm các cơ chế linh hoạt hơn như ABAC (Attribute-Based Access Control).
Tóm lại, cần có sự hiểu biết về hệ thống để quyết định xem có dùng RBAC hay không. Nếu hệ thống thường sử dụng các role cố định, có thể áp dụng RBAC. Tuy nhiên, với hệ thống phức tạp, nhiều ngoại lệ, không nên sử dụng RBAC, có thể thử sang ABAC hoặc hệ thống phân quyền phức tạp hơn.
Giới thiệu về Casbin
Casbin là một thư viện mã nguồn mở được sử dụng để thực hiện kiểm soát truy cập trong các ứng dụng và hệ thống phần mềm. Nó cung cấp một cơ chế linh hoạt và dễ sử dụng để xác định và quản lý quyền truy cập của người dùng và các đối tượng trong hệ thống.
Casbin cho phép bạn xác định các quy tắc truy cập bằng cách định nghĩa các chính sách (policy) dưới dạng các cặp (subject, object, action). Subject đại diện cho người dùng hoặc vai trò, object đại diện cho tài nguyên hoặc đối tượng cần truy cập, và action đại diện cho hành động mà subject có thể thực hiện trên object.
Trong Casbin, mô hình kiểm soát truy cập được trừu tượng hóa thành tệp CONF dựa trên siêu mô hình PERM (Policy, Effect, Request, Matchers). Mô hình PERM bao gồm bốn nền tảng (Policy, Effect, Request, Matchers) mô tả mối quan hệ giữa tài nguyên và người dùng.
Request
Xác định các tham số yêu cầu. Yêu cầu ít nhất một subject (thực thể được truy cập), object (tài nguyên được truy cập) và action (phương thức truy cập).
Policy
Định nghĩa tên và thứ tự các trường trong Policy rule, gồm các tham số subject, object, action, effect.
Matcher
Kiểm tra các quy tắc của Request và Policy.
Ví dụ: m = r.sub == p.sub && r.act = p.act && r.obj == p.obj
Hiểu đơn giản thì nếu có policy khớp với request thì trả về kết quả policy (p.eft).
Effect
Lấy kết quả p.eft từ Matcher để trả về kết quả cuối cùng, xem có được truy cập hay không.
Ví dụ: e = some(where(p.eft == allow))
Nghĩa là nếu kết quả so khớp p.eft có kết quả là allow (với 1 vài policy), thì kết quả cuối cùng là true.
Để tìm hiểu kĩ hơn, các bạn có thể đọc thêm ở https://casbin.org/docs/overview
1 số thư viện và framework mình dùng
Casbin: https://casbin.org/docs/overview
Iris: https://www.iris-go.com
MongoDB Adapter: https://github.com/casbin/mongodb-adapter
Cấu trúc thư mục
test_rbac/
examples/
rbac_model.conf
models/
models.go
rbac/
rbac.go
courses.go
blog.go
routes/
routes.go
go.mod
go.sum
main.go
Thiết lập cơ bản
File conf:
// rbac_model.conf
[request_definition]
r = sub, obj, act
r2 = sub, obj, act
r3 = sub, obj, act
[policy_definition]
p = sub, obj, act
p2 = sub, obj, act
p3 = sub, obj, act
[role_definition]
g = _, _
g2 = _, _, _
g3 = _, _, _
[policy_effect]
e = some(where (p.eft == allow))
e2 = some(where (p.eft == allow)) && !some(where (p.eft == deny))
e3 = some(where (p.eft == allow)) && !some(where (p.eft == deny))
[matchers]
m = keyMatch2(r.obj, p.obj) && r.act == p.act && g(r.sub, p.sub)
m2 = keyMatch2(r2.obj, p2.obj) && r2.act == p2.act && g2(r2.sub, keyGet2(r2.obj, p2.obj, 'id'), p2.sub)
m3 = keyMatch2(r3.obj, p3.obj) && r3.act == p3.act && g3(r3.sub, keyGet2(r3.obj, p3.obj, 'id'), p3.sub)
Hẳn mọi người sẽ thắc mắc tại sao xuất hiện g hay role_definition ở đây, và nó là gì? Về cơ bản, nó là cách giúp chúng ta nhóm lại các thông số cần so sánh, và cũng so sánh ở matcher. Ví dụ, hệ thống có role User, khi thêm 1 người dùng mới ta chỉ cần cho người dùng đó vào group User là xong.
Người đó sẽ có role là User
Thiết lập port:
// main.go
package main
import (
"github.com/kataras/iris/v12"
"github.com/lits-06/test_rbac/routes"
)
func main() {
app := iris.New()
routes.RegisterRoute(app)
app.Listen(":8080")
}
Thiết lập adapter:
// rbac.go
package rbac
import (
"github.com/casbin/casbin/v2"
mongodbadapter "github.com/casbin/mongodb-adapter/v3"
"github.com/kataras/iris/v12"
)
var rbac *casbin.Enforcer
func Setup() (*casbin.Enforcer) {
a, err := mongodbadapter.NewAdapter("mongodb://development:testpassword@localhost:27017")
if err != nil {
panic(err)
}
r, err := casbin.NewEnforcer("examples/rbac_model.conf", a)
if err != nil {
panic(err)
}
r.LoadPolicy()
rbac = r
return r
}
Thiết lập các route:
// routes.go
package routes
import (
"github.com/kataras/iris/v12"
"github.com/lits-06/test_rbac/rbac"
)
func RegisterRoute(app *iris.Application) {
r := rbac.Setup()
app.Post("/add_role", rbac.AddRole())
app.Post("/add_role_for_user", rbac.AddRoleForUser())
app.Post("/delete_role_for_user", rbac.DeleteRoleForUser())
app.Post("/add_api_for_role", rbac.AddAPIForRole())
app.Post("/delete_api_for_role", rbac.DeleteAPIForRole())
app.Post("/add_role_for_api", rbac.AddRoleForAPI())
app.Post("/delete_role_for_api", rbac.DeleteRoleForAPI())
app.Post("/delete_role", rbac.DeleteRole())
app.Get("/admin", rbac.CheckAuth(), rbac.Notification())
app.Post("/add_course", rbac.AddCourse())
// nhập user_id: []string và class_id: string
app.Post("/add_trainer", rbac.AddTrainer())
app.Post("/add_student", rbac.AddStudents())
app.Post("/delete_trainer", rbac.DeleteTrainer())
app.Post("/delete_student", rbac.DeleteStudent())
// nhập user_id: string
app.Post("/courses/:id", rbac.CheckClassPermission(), rbac.ClassNotification())
// nhập user_id: string
app.Post("/add_new_blog", rbac.AddBlog())
app.Post("/posts/:id", rbac.CheckBlogPermission(), rbac.BlogNotification())
// nhập user_id: string và blog_id: []string
app.Post("/delete_blog", rbac.DeleteBlog())
r.SavePolicy()
}
Vậy là đã thiết lập xong. Giờ thì bắt tay vào code các chức năng thôi.
Thêm role cho hệ thống
Chúng ta muốn nhập vào gồm: tên role, các endpoint API và các method. Trong ví dụ này mình sẽ nhập bằng JSON nên chúng ta cần 1 struct để chứa thông tin.
// rbac.go
type RoleData struct {
Role string `json:"role"`
API []string `json:"api"`
Method []string `json:"method"`
}
func AddRole() iris.Handler {
return func(ctx iris.Context) {
var data RoleData
// đọc dữ liệu và gán vào data
if err := ctx.ReadJSON(&data); err != nil {
ctx.JSON("can not get data")
return
}
// nếu không cung cấp endpoint, ta sẽ thêm endpoint mặc định
if data.API == nil {
data.API = append(data.API, "http://localhost:8080")
}
// thêm các policy rule
for _, api := range data.API {
for _, method := range data.Method {
_, err := rbac.AddPolicy(data.Role, api, method)
if err != nil {
ctx.JSON("ERROR")
return
}
}
}
ctx.WriteString("success added role: " + data.Role)
}
}
Cùng test thử xem nhé:
Thêm, xóa role cho người dùng
Chúng ta muốn nhập vào các user ID và tên role được gán. Thêm UserRole struct:
// rbac.go
type UserRole struct {
UserID []string `json:"user_id"`
Role string `json:"role"`
}
func AddRoleForUser() iris.Handler {
return func(ctx iris.Context) {
var data UserRole
if err := ctx.ReadJSON(&data); err != nil {
ctx.JSON("can not get data")
return
}
for _, id := range data.UserID {
_, err := rbac.AddGroupingPolicy(id, data.Role)
if err != nil {
ctx.WriteString("cans not add role for this user")
return
}
}
ctx.WriteString("success")
}
}
func DeleteRoleForUser() iris.Handler {
return func(ctx iris.Context) {
var data UserRole
if err := ctx.ReadJSON(&data); err != nil {
ctx.JSON("can not get data")
return
}
for _, id := range data.UserID {
_, err := rbac.RemoveGroupingPolicy(id, data.Role)
if err != nil {
ctx.WriteString("cans not delete role for this user")
return
}
}
ctx.WriteString("success")
}
}
Test:
Thêm, xóa endpoint cho role
Chúng ta muốn nhập vào gồm: tên role, các endpoint API và các method.
Thêm endpoint:
// rbac.go
func AddAPIForRole() iris.Handler {
return func(ctx iris.Context) {
var data RoleData
if err := ctx.ReadJSON(&data); err != nil {
ctx.JSON("can not get data")
return
}
// nếu chưa có method nào thì thêm method mặc định GET
if data.Method == nil {
data.Method = append(data.Method, "GET")
}
for _, api := range data.API {
for _, method := range data.Method {
_, err := rbac.AddPolicy(data.Role, api, method)
if err != nil {
ctx.JSON("ERROR")
return
}
}
}
ctx.WriteString("success")
}
}
Xóa endpoint:
// rbac.go
type RoleAPI struct {
Role string `json:"role"`
API []string `json:"api"`
}
func DeleteAPIForRole() iris.Handler {
return func(ctx iris.Context) {
var data RoleAPI
if err := ctx.ReadJSON(&data); err != nil {
ctx.JSON("can not get data")
return
}
allAction := rbac.GetAllActions()
for _, api := range data.API {
for _, action := range allAction {
rbac.RemovePolicy(data.Role, api, action)
}
}
// nếu không còn endpoint nào thì thêm http://localhost:8080
filteredPolicy := rbac.GetFilteredPolicy(0, data.Role)
if (len(filteredPolicy)) == 0 {
_, err := rbac.AddPolicy(data.Role, "http://localhost:8080", "GET")
if err != nil {
ctx.JSON("ERROR")
return
}
}
ctx.WriteString("success delete")
}
}
Test:
Thêm, xóa role cho endpoint
Với thêm role, ta sẽ nhập vào endpoint, role và method:
// rbac.go
type APIData struct {
API string `json:"api"`
Role []string `json:"role"`
Method []string `json:"method"`
}
func AddRoleForAPI() iris.Handler {
return func(ctx iris.Context) {
var data APIData
if err := ctx.ReadJSON(&data); err != nil {
ctx.JSON("can not get data")
return
}
for _, role := range data.Role {
for _, method := range data.Method {
_, err := rbac.AddPolicy(role, data.API, method)
if err != nil {
ctx.JSON("ERROR")
return
}
}
}
ctx.WriteString("success")
}
}
Còn với xóa role, ta chỉ cần nhập endpoint và các role:
// rbac.go
type APIRole struct {
API string `json:"api"`
Role []string `json:"role"`
}
func DeleteRoleForAPI() iris.Handler {
return func(ctx iris.Context) {
var data APIRole
if err := ctx.ReadJSON(&data); err != nil {
ctx.JSON("can not get data")
return
}
allAction := rbac.GetAllActions()
for _, role := range data.Role {
for _, action := range allAction {
_, err := rbac.RemovePolicy(role, data.API, action)
if err != nil {
ctx.JSON("ERROR")
return
}
}
}
ctx.WriteString("success delete")
}
}
Test:
Xóa role
Để xóa role, chúng ta chỉ cần nhập tên role đó:
// rbac.go
type Role struct {
Role string `json:"role"`
}
func DeleteRole() iris.Handler {
return func(ctx iris.Context) {
var data Role
err := ctx.ReadJSON(&data)
if err != nil {
ctx.JSON("can not get data")
return
}
ok, err := rbac.DeleteRole(data.Role)
if err != nil {
ctx.JSON("ERROR")
return
}
// nếu không có role thì in ra
if !ok {
ctx.JSON("do not have role: " + data.Role)
return
}
ctx.JSON("success delete role: " + data.Role)
}
}
Test:
Xác thực
Mình sẽ add lại role user. Với admin, anh ta có thể truy cập mọi đường dẫn con của localhost:8080 tuy nhiên user chi có thể truy cập được localhost:8080 mà thôi. Ta sẽ nhập vào UserID để test:
// rbac.go
type User struct {
UserID string `json:"user_id"`
}
func CheckAuth() iris.Handler {
return func(ctx iris.Context) {
var data User
if err := ctx.ReadJSON(&data); err != nil {
ctx.JSON("can not get data")
return
}
path := "http://localhost:8080" + ctx.Path()
ok, err := rbac.Enforce(data.UserID, path, "GET")
if !ok || err != nil {
ctx.WriteString("ERROR")
return
}
ctx.Next()
}
}
func Notification() iris.Handler {
return func(ctx iris.Context) {
ctx.WriteString("Hello")
}
}
Test:
Kết
Bài viết khá dài rồi, nên còn 1 vài vấn đề nữa mình sẽ viết ở phần 2 nha.
Bình luận