Real-time authorization with Enterprise OPA and gRPC

5 min read

What you will learn

In this article, you will learn about how to achieve high-throughput, real-time authorization. You should gain a basic understanding of the different protocols for interacting with the Open Policy Agent (OPA) and Styra Enterprise OPA APIs, as well as how and when to use different options. We will also cover the strengths of different protocol choices, and where they may make sense in your system architecture.

Why you need high-throughput, real-time authorization


Modern applications and cloud services require near real-time authorization given the application or service may need to make many thousands of authorization requests per second. This type of workload is incredibly sensitive to latency– the service often cannot proceed with a request until it knows whether it’s allowed to!

The Open Policy Agent (OPA) was designed around a distributed authorization model, placing OPA instances next to the services that need authorization decisions. This greatly reduces request/response latency for most services, and improves the overall user experience for people and systems downstream of that service.

Enterprise OPA takes this focus on reducing latency a few steps further. It includes both runtime optimizations to execute Rego policies faster, and adds support for an efficient network protocol: gRPC.

What is gRPC?

gRPC is a remote procedure call framework, allowing applications to treat remote services as if they were local objects. This is often touted as a way to make writing distributed applications easier, but for EOPA’s purposes, the main advantage is providing a well-supported definition system for RPC endpoints, and seamless integration of the efficient protobuf message encoding format.

gRPC also uses HTTP/2 as its transport layer by default. This means more efficient network communication between EOPA and services using its gRPC API, and native support for streaming communications between them. EOPA offers a handful of bidirectional streaming gRPC endpoints for even better performance in high-throughput or long-lived connections between services. We’ll talk about these different communication options, and their ideal use cases in the next section.

Improving the performance of OPA using gRPC

EOPA offers two gRPC options for more efficient communication with clients: unary gRPC endpoints, and streaming gRPC endpoints. Unary gRPC calls work almost exactly like calls to HTTP REST endpoints, just with a more efficient message format. Streaming gRPC calls however, offer the ability to send multiple requests over the same connection. EOPA also allows some batching of requests made over the streaming endpoints, which further improves performance.

The graphic above shows what the traffic patterns look like for each communication method. Unary gRPC is generally more efficient than HTTP, and streaming gRPC dramatically improves over both unary gRPC and HTTP, because it doesn’t have to constantly reconnect to the EOPA server. In practice, streaming works best for high-throughput scenarios, and for long lived connections between services.

Making an authorization request via EOPA gRPC vs Open Policy Agent HTTP REST API

To see what the setup and usage looks like for each flavor of communication protocol, we’ll be using the simple policy shown below:

package authz

import future.keywords.if
import future.keywords.in

admins := {"alice", "betty"}
users := {"alice", "betty", "bob", "charlie"}

default allow := false

# Admins can do reads or writes.
allow if {
    input.user in admins
}

# Normal users can edit their profile.
allow if {
    input.user in users
    input.path == concat("", ["/users/", input.user])
}

Before running Enterprise OPA, we will need to set the EOPA_LICENSE_KEY environment variable.

Trial License

To evaluate Enterprise OPA, you can obtain a trial license by either method:

Running EOPA

We can start up EOPA with this policy and the gRPC server enabled using the following CLI command:

./eopa run -s --set plugins.grpc.addr=localhost:9090 policy.rego

Authorization with HTTP Requests

For one-off or low-frequency requests between OPA and a client program or service, the standard OPA REST API is a perfect fit. Developers can take advantage of industry standard tools for prototyping and debugging HTTP APIs, and get going quickly using tutorials from the OPA docs.

Example Golang code to query the above policy using HTTP:

package main

import (
   "bytes"
   "fmt"
   "io"
   "log"
   "net/http"
)

func main() {
   jsonStr := []byte(`{"input": {"user": "alice", "path": "/users/bob"}}`)
   req, err := http.NewRequest("POST", "http://localhost:8181/v1/data/authz/allow", bytes.NewBuffer(jsonStr))
   if err != nil {
       log.Fatal(err)
   }
   req.Header.Set("Content-Type", "application/json")

   client := &http.Client{}
   resp, err := client.Do(req)
   if err != nil {
       log.Fatal(err)
   }
   defer resp.Body.Close()

   body, err := io.ReadAll(resp.Body)
   if err != nil {
       log.Fatal(err)
   }
   fmt.Println("/authz/allow", string(body))
}

The result of running the above code against EOPA should look like:

/authz/allow {"result":true}

As Enterprise OPA is drop-in compatible with open source OPA, all of the same tools and techniques used for deploying open source OPA apply, and will scale noticeably further, thanks to the runtime improvements.

Authorization With EOPA’s Unary gRPC Endpoints

EOPA provides several unary gRPC endpoints. These behave almost identically to the equivalent REST endpoints in OPA, but generally are more efficient in terms of both network traffic and processing requirements for the EOPA server. (Deserializing JSON can be expensive!)

Below is an example of querying the DataService/GetData endpoint, same as for HTTP:

package main

import (
   "context"
   "fmt"
   "log"

   datav1 "github.com/philipaconrad/load-grpc-example/proto/gen/go/eopa/data/v1"

   "google.golang.org/grpc"
   "google.golang.org/grpc/credentials/insecure"
   "google.golang.org/protobuf/types/known/structpb"
)

func main() {
   ctx := context.Background()
   // Connect to the Enterprise OPA instance.
   conn, err := grpc.Dial("localhost:9090", grpc.WithTransportCredentials(insecure.NewCredentials()))
   if err != nil {
       log.Fatalf("failed to dial the EOPA server: %v", err)
   }
   defer conn.Close()
   client := datav1.NewDataServiceClient(conn)

   // Prepare a normal gRPC Data Get request:
   s, _ := structpb.NewStruct(map[string]interface{}{
       "user": "alice",
       "path": "/users/betty",
   })
   doc := &datav1.InputDocument{Document: s}
   // Send unary gRPC message, and block until response arrives.
   resp, err := client.GetData(ctx, &datav1.GetDataRequest{Path: "/authz/allow", Input: doc})
   if err != nil {
       log.Fatalf("GetData failed: %v", err)
   }
   resultDoc := resp.GetResult()
   path := resultDoc.GetPath()
   allowed := resultDoc.GetDocument().GetBoolValue()
   fmt.Println(path, allowed)
}

The result of running the above code against EOPA should look like:

/authz/allow true

These endpoints are great for use cases where the HTTP API is becoming a bottleneck, or for services where serialization and deserialization overheads of JSON are becoming a problem.

Authorization With EOPA’s Streaming gRPC Endpoints

In addition to the unary gRPC endpoints, EOPA also provides a set of streaming gRPC endpoints, optimized for bulk processing of requests in a transactional style. These allow high-throughput data updates and can process multiple rule queries in parallel over a single connection.

Below is an example of querying the DataService/StreamingRW endpoint, with 3x requests sent over the same connection:

package main

import (
   "context"
   "fmt"
   "log"

   datav1 "github.com/philipaconrad/load-grpc-example/proto/gen/go/eopa/data/v1"

   "google.golang.org/grpc"
   "google.golang.org/grpc/credentials/insecure"
   "google.golang.org/protobuf/types/known/structpb"
)

func main() {
   ctx := context.Background()
   // Connect to the Enterprise OPA instance.
   conn, err := grpc.Dial("localhost:9090", grpc.WithTransportCredentials(insecure.NewCredentials()))
   if err != nil {
       log.Fatalf("failed to dial the EOPA server: %v", err)
   }
   defer conn.Close()
   client := datav1.NewDataServiceClient(conn)
   sclient, err := client.StreamingDataRW(ctx)
   if err != nil {
       log.Fatal(err)
   }

   inputs := []map[string]interface{}{
       {"user": "alice", "path": "/users/bob"},
       {"user": "bob", "path": "/users/alice"},
       {"user": "bob", "path": "/users/bob"},
   }

   for _, v := range inputs {
       // Prepare a normal gRPC Data Get request:
       s, _ := structpb.NewStruct(v)
       getReq := datav1.GetDataRequest{Path: "/authz/allow", Input: &datav1.InputDocument{Document: s}}
       // Embed it into a streaming read/write message:
       req := datav1.StreamingDataRWRequest{Reads: []*datav1.StreamingDataRWRequest_ReadRequest{|{Get: &getReq}}}
       // Send message...
       if err := sclient.Send(&req); err != nil {
           log.Fatal(err)
       }
       // ...See what we got in response.
       resp, err := sclient.Recv()
       if err != nil {
           log.Fatal(err)
       }
       // Print result(s):
       for _, read := range resp.GetReads() {
           resultDoc := read.Get.GetResult()
           path := resultDoc.GetPath()
           allowed := resultDoc.GetDocument().GetBoolValue()
           fmt.Println(path, allowed)
       }
   }

   // Send the close message, and make sure there were no errors.
   if err := sclient.CloseSend(); err != nil {
       log.Fatal(err)
   }
}

The result of running the above code against EOPA should look like:

/authz/allow true
/authz/allow false
/authz/allow true

These endpoints are excellent for real-time authorization which have high-performance, high-throughput use cases where latency matters most. Unfortunately, this is also the highest-effort option to implement, but the implementation complexity is reduced somewhat by shared request types across the unary and streaming API endpoints.

Case Study: Real-World Examples of Real-Time Authorization in Action

In a proof-of-concept exercise with Miro, the streaming gRPC endpoints provided an extreme speed boost for their workload, a several orders of magnitude improvement over open source OPA HTTP interface. For their workload (one involving frequent data mutations interleaved with rule queries), the speedups from using the gRPC interfaces meant that they could use EOPA for entirely different use cases than were possible before.

If your authorization use case requires a large volume of real-time requests, then Enterprise OPA gRPC endpoints are a great fit. To get started on integrating these gRPC calls into your applications, try out the gRPC with Go tutorial in the official EOPA docs, and head over to the Styra Community Slack if you have questions.

Cloud native
Authorization

Entitlement Explosion Repair

Join Styra and PACLabs on April 11 for a webinar exploring how organizations are using Policy as Code for smarter Access Control.

Speak with an Engineer

Request time with our team to talk about how you can modernize your access management.