12/08/2018, 18:11

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             </div>
            
            <div class=

0