Permission trong GraphQL & Prisma
Với bất kì hệ thống backend nào, permission và authentication luôn là những vấn đề quan trọng cần quan tâm. Với ai đã từng tiếp xúc qua với GraphQL thì đều có thể nhận ra điều này - GraphQL là một hệ thống mở. Nhìn vào một hệ thống GraphQL thì phía client hoàn toàn có thể thấy được toàn bộ các ...
Với bất kì hệ thống backend nào, permission và authentication luôn là những vấn đề quan trọng cần quan tâm.
Với ai đã từng tiếp xúc qua với GraphQL thì đều có thể nhận ra điều này - GraphQL là một hệ thống mở. Nhìn vào một hệ thống GraphQL thì phía client hoàn toàn có thể thấy được toàn bộ các API cũng như cấu trúc data của hệ thống. Điều này có thể thay đổi sau này - nhưng hiện giờ thì toàn bộ các hoạt động cũng như dữ liệu của hệ thống đều bị phơi bày ra hết.
Trong bài viết này, mình sẽ đề cập đến 1 vài cách cơ bản xử lý với permission khi sử dụng GraphqlQL thông qua Prisma.
Schema hệ thống
Với toàn bộ bài viết này, mình sẽ chỉ sử dụng 1 cấu trúc dữ liệu để có thể dễ dàng so sánh được các cách tiếp cận với nhau.
Schema trong bài sẽ bao gồm 3 bảng: User, AuthProvider (phương thức đăng nhập của 1 user - có thể là bằng user - password hay 3rd party login ...) và bảng App. Các app sẽ có owner là 1 User.
type User { id: ID! @unique name: String role: Role! @default(value: "USER") providers: [AuthProvider!]! } type AuthProvider { id: ID! @unique user: User! type: ProviderType! uuid: String @unique token: String email: String! password: String } type App { id: ID! @unique type: AppType! owner: User! members: [Member!]! info: String! }
Nhìn vào 3 schema phía trên, ta sẽ thấy ngay được hàng loạt vấn đề - đó là bất cứ người nào có quyền truy cập vào GraphQL Server thì cũng sẽ có mọi người để xem, chỉnh sửa ... tóm lại là làm MỌI THỨ với dữ liệu của bạn.
Để tằng tính an toàn cho API, ít nhất ta có thể thêm vào một vài luật như sau:
- UpdateAppInfo sẽ chỉ có owner có quyền.
- xem thông tin của một app sẽ chỉ có owner và member có quyền.
- KHÔNG trả về password khi phía client lấy thông tin user.
Cách số 1: Trâu bò
Nhắc lại một chút về cấu trúc hệ thống
Trong hệ thống của mình, Prisma chỉ là một lớp (layer) ORM ở giữa, ta vẫn cần có một GraphQL Server chính và ở đây thì mình sử dụng GraphQL-Yoga.
Thêm vào đó, ta sử dụng thêm GraphQL-binding để tự động sinh ra các hàm CRUD cơ bản cho API của mình.
Xử lý permission trong resolver
Cách đơn giản nhất đó là ta có thể implement logic kiểm tra permission khi viết resolver:
const Mutation = { updateAppInfo: async (parent, { id, info }, context, info) => { const userId = getUserId(context); const app = await context.db.query.app({ where: { id: id } }); if(userId == app.owner.id) { // update logic } else throw new Error( // ... not found error ) } }
Ta tìm bản ghi app và so sánh app.owner.id với id của current user rồi sau đó mới cho phép thực hiện logic update.
Với cách tiếp cận này, về lâu dài, resolver của ta sẽ trở nên lộn xộn và trùng lặp với hàng loạt logic kiểm tra permission.
Cách số 2: Sử dụng GraphQL Directive
Với cách thứ 2 này, ta sẽ sử dụng một tính năng khác của GraphQL - Directive - để nhúng trực tiếp việc khai báo permission vào thẳng schema
directive @isOwner on FIELD | FIELD_DEFINITION directive @privateField on FIELD | FIELD_DEFINITION // ta sử dụng các directive kể trên để nhúng thẳng vào mutation type Mutation { updateAppInfo(id: ID!, info: String!): App! @isOwner } // cũng như nhúng thẳng vào type definition trong schema type AuthProvider { id: ID! @unique user: User! type: ProviderType! uuid: String @unique token: String email: String! password: String @privateField }
So với cách đầu tiên, ta có thể thấy ưu điểm lớn nhất của cách này: đó là từ phía client nhìn vào thì hoàn toàn có thể thấy được các permission này mà không phải đọc vào code bên trong resolver. Cùng với đó, việc sử dụng lại 1 logic permission chỉ đơn giản là copy lại directive đó đến nơi mình muốn