prisma reverse proxy

outline

prisma has been in contact for some time, and several other graphql interface automatic generation frameworks have been used during this period. Generally speaking, prisma generates more abundant interfaces, is more convenient to use, and has low coupling with databases.

prisma document: https://www.prisma.io/docs (Version 1.34 at the time of writing)

Why prisma's Reverse Agent

prisma services generate interfaces automatically, but these interfaces are not recommended to be exposed directly to the front-end for use, because in actual projects, the most basic thing is to authenticate and control the interfaces. There are even other requirements, so it is impossible to complete all functions only with the automatically generated interfaces.

So, when using prisma service, we usually encapsulate another layer (called gateway) to authenticate, authorize and so on. Only legitimate requests can be forwarded to prisma service. prisma service itself can export client SDK to facilitate gateway writing. At present, it supports four kinds of lattices. Type (javascript, typescript, golang, flow), javascript and typescript are client SDK with more complete functions, golang with weaker functions, and flow has not been tried.

When I wrote gateway with golang client SDK, I found that the library related to golang's graphql server was not as perfect as js/ts. So I wanted to intercept the front-end graphql request by reverse proxy, and then directly forward the request content to the PRISMA service after doing the corresponding operation. This method does not use prisma. The generated client SDK also breaks through the language limitation. Besides golang, java, C, other languages can also be used as the gateway of prisma.

Reverse proxy example (by golang)

Goang's gin web Services Framework as a gateway. Used in Authentication gin-jwt Middleware. The reverse proxy and privilege sections do not use the existing framework.

Examples of the entire gateway include:

  1. Prisma service (prisma + mysql): This section has a ready docker image, just configure the tables and fields of the sample
  2. gateway (golang gin): golang gin's api service

prisma service

  1. prisma.yml

    endpoint: http://${env:PRISMA_HOST}:${env:PRISMA_PORT}/illuminant/${env:PRISMA_STAGE}
    datamodel: datamodel.prisma
    
    secret: ${env:PRISMA_MANAGEMENT_API_SECRET}
    
    generate:
      - generator: go-client
        output: ./
  2. .env

    PRISMA_HOST=localhost
    PRISMA_PORT=4466
    PRISMA_STAGE=dev
    PRISMA_MANAGEMENT_API_SECRET=secret-key
  3. datamodel.prisma

    type User {
      id: ID! @id
      name: String! @unique
      realName: String!
      password: String!
    
      createdAt: DateTime! @createdAt
      updatedAt: DateTime! @updatedAt
    }
  4. docker-compose.yml

    version: '3'
    services:
      illuminant:
        image: prismagraphql/prisma:1.34
        # restart: always
        ports:
        - "4466:4466"
        environment:
          PRISMA_CONFIG: |
            port: 4466
            managementApiSecret: secret-key
            databases:
              default:
                connector: mysql
                host: mysql-db
                user: root
                password: prisma
                # rawAccess: true
                port: 3306
                migrations: true
    
      mysql-db:
        image: mysql:5.7
        # restart: always
        environment:
          MYSQL_ROOT_PASSWORD: prisma
        volumes:
          - mysql:/var/lib/mysql
    volumes:
      mysql: ~

The above files can be placed in the same directory, including all the files required by prisma services and mysql services.

gateway service

The gateway service is the key and the extension part in the future. It is written in the golang gin framework.

Overall process

  1. HTTP request
  2. route routing
  3. Certification Check
  4. Permission Check
  5. Request forwarding PRISMA service (this step is usually forwarded to prisma, if there are upload/download, or statistics and other requirements, need to write API)
  6. Return to Response

Authentication

authMiddleware := controller.JwtMiddleware()
apiV1 := r.Group("/api/v1")

// no auth routes
apiV1.POST("/login", authMiddleware.LoginHandler)

// auth routes
authRoute := apiV1.Group("/")
authRoute.GET("/refresh_token", authMiddleware.RefreshHandler)
authRoute.Use(authMiddleware.MiddlewareFunc())
{
  // proxy prisma graphql
  authRoute.POST("/graphql", ReverseProxy())
}

/ api/v1/graphql is accessible only when jwt authentication is satisfied.

Reverse proxy

func ReverseProxy() gin.HandlerFunc {

  return func(c *gin.Context) {
    director := func(req *http.Request) {
      req.URL.Scheme = "http"
      req.URL.Host = primsa-host
      req.URL.Path = primsa-endpoint
      delete(req.Header, "Authorization")
      req.Header["Authorization"] = []string{"Bearer " + primsa-token}

    }

    // Parse out the content of body and check the permission
    body, err := c.GetRawData()
    if err != nil {
      fmt.Println(err)
    }

    // Perform permission check on body
    // Check the permission, parse the function requested in graphql, and then determine whether there is permission
    // The current way is to judge permissions based on the name of the function in the request, that is, the CURD permissions of the table can only be judged, and the field permissions in the table can not be checked.
    // If the permission check fails, return directly and do not forward the following request

    // Deserialize the body back into the request and forward it to the prisma service
    c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))

    proxy := &httputil.ReverseProxy{Director: director}
    proxy.ModifyResponse = controller.RewriteBody
    proxy.ServeHTTP(c.Writer, c.Request)
  }
}

Jurisdiction

// Check permissions
func CheckAuthority(body []byte, userId string) bool {
        var bodyJson struct {
                Query string `json:"query"`
        }
        log := logger.GetLogger()
        if err := json.Unmarshal(body, &bodyJson); err != nil {
                log.Error("body convert to json error: %s", err.Error())
                return false
        }

        graphqlFunc := RegrexGraphqlFunc(bodyJson.Query)
        if graphqlFunc == "" {
                return false
        }

        // Here userId is parsed from jwt and then judged whether the user has permission or not.

        if graphqlFunc == "users" {
                return false
        }
        return true
}

// Functions matching graphql requests
func RegrexGraphqlFunc(graphqlReq string) string {
        graphqlReq = strings.TrimSpace(graphqlReq)
        // reg examples:
        // { users {id} }
        // { users(where: {}) {id} }
        // mutation{ user(data: {}) {id} }
        var regStrs = []string{
                `^\{\s*(\w+)\s*\{.*\}\s*\}$`,
                `^\{\s*(\w+)\s*\(.*\)\s*\{.*\}\s*\}$`,
                `^mutation\s*\{\s*(\w+)\s*\(.*\)\s*\{.*\}\s*\}$`,
        }

        for _, regStr := range regStrs {
                r := regexp.MustCompile(regStr)
                matches := r.FindStringSubmatch(graphqlReq)
                if matches != nil && len(matches) > 1 {
                        return matches[1]
                }
        }

        return ""
}

Here permission checking is an implementation idea, not the final code.
It is only a temporary solution, not the best way, to match the function in the request with regular expressions.
The best way to do this is to use the graph QL parsing library corresponding to golang to parse the structure of the request, and then determine the privileges of the parsed function.

summary

The way of reverse proxy is to break through the limitation of prisma client SDK. If the client SDK is improved later, it is more reliable to develop gateway based on client SDK.

Tags: Go MySQL SDK JSON Javascript

Posted on Sun, 25 Aug 2019 21:47:39 -0700 by chris_s_22