Poor Man’s Authorization: How to Implement RBAC for REST API with OPA

Written by shepelev | Published 2022/01/11
Tech Story Tags: rbac | rest-api | javascript | nginx | authentication | jwt | rest-api-with-opa | implement-rbac

TLDRThe RBAC is a way to show the possibility of user restrictions in applications. It is possible to close key functions without time-consuming integrations with security systems. With the rapid implementation of Proof of Concept applications or functions, your security implementation often becomes less important. There are three rules in authorization, which say that the reader can read and change, the editor can read data, and the administrator can do everything. This is more than what it is done in theory than what is really, this is the theory.via the TL;DR App

When it comes to application permissions, two results emerge from this situation:
  • Either there are bindings to certain roles/scopes in the application code;
  • Or the developer grows a beard and starts using phrases like abac, xacml, and an access matrix;
If you are interested in how you can build RBAC from available tools for any service that respects REST, you’re welcome.

What is it for?

  • POC: With the rapid implementation of Proof of Concept applications or functions, your security implementation often becomes less important. And sometimes these functions are required at least to implement authorization to demonstrate the possibility of user restrictions;
  • Resources: When deploying in a resource-constrained environment, there is a strong desire to omit the heaviest functions, including the computation of access rights. It would be great to get a solution that will make it possible to close key functions without time-consuming integrations with security systems;
  • Firewall: Using this function as a firewall at the entrance to the cluster can partly ensure an additional layer of security. For example, applications that do not implement security functions can get into the cluster. Thus, you can put two blocks in the configuration: trusted applications that ensure security, as well as all the others that are closed by this block, albeit with serious reservations.
In short, this entire article can be fit into the following phrase:
When processing a request in Nginx, we send an access request to the OPA before sending it to the service and receive the authorization result. If access is allowed, the request is sent to the service.

Application

So, let’s consider an example with an application and its placement.
Suppose we have a cluster containing two applications:
  • API gateway;
  • Business application with REST API;
The application has a REST API with CRUD operations:
  • To get data. HTTP method GET.
  • To create data. HTTP method POST.
  • To change data. HTTP method PUT.
  • To delete data. HTTP method DELETE.
Now let’s form a minimal access matrix:
  • Only a user with reader rights can read data;
  • Only a user with editor rights can read, create and change data;
  • Only the administrator can perform all of the above operations, as well as the delete operation;

Authorization

Now let’s figure out how to define the possibility of accessing data.
To make a decision, you will need the following data:
  • Who?
  • What does he want to do?
  • What data will he use?
In our case, this data can be interpreted:
  • User
  • Action
  • Data
The result will be a positive or negative decision.
Based on this decision, you can conclude what the user can perform within the business application.
Now let’s go back to our example with the application and figure out where to get the data for making such a decision.
User. As a user, it is very convenient to use a JWT token as a validated identity snapshot.
Recently, Keycloak and its SSO Redhat implementation have been gaining in popularity, so I’m going to proceed from the Keycloak token structure.
Action. It is very convenient to use the action marker to operate with the classic REST notation and assume that the methods.
GET is to read, POST/PUT is to create and change, and DELETE is to delete.
Data. In the case of a proxy, it is convenient to interpret data as a route. That is, the route that a call takes is our data.

Gateway, Authorization, Application

Now let’s put together all the above puzzles to form a picture.
If we want to perform authorization at the proxy/gateway level according to requests from users, we have all the initial data for checking access rights.
That is, if we assume that Gateway can perform the authorization request, all that remains is to add a new puzzle to our picture – the authorization module.
Thus, our chain turns into the following sequence:
  1. The user has received his identity token, and we assume that it contains all the necessary information about the user. With this token, the user makes a request to the business application and gets to Gateway.
  2. Gateway needs to form a request for access rights. To do this, it parses the request into parts:
    - It takes the token from the header and deserializes it, thus forming data about the user;
    - It separates the HTTP method from the request and says that this is the action performed by the user;
    - It forms data from the request path;
  3. There are three rules in authorization, which say that the reader can read data, the editor can read and change data, and the administrator can do everything;
  4. If access is allowed, the request is sent to the business application.

Implementation

That’s it. The theory is done. To be honest, there is much more theory than implementation. This is what this decision really impresses me with.
I will use OPA as an authorization module – https://www.openpolicyagent.org
And I will take Nginx for Gateway – http://nginx.org
As a side note, OPA is gaining popularity in filtering requests, and there are modules for Envoy – https://github.com/open-policy-agent/opa-envoy-plugin, Traefic – https://doc.traefik.io/traefik-enterprise/v2.4/middlewares/opa/
Nginx
In my case, the main Nginx configuration does not contain any additional manipulations.
JWT
I use Keycloak as my token publisher. However, for clarity of interaction, I’ve added the following methods to Nginx:
  • /jwt/create
    – Create a JWT token without roles;
  • /jwt/create/viewer
    – Create a JWT token with the viewer role: “viewer”;
  • /jwt/create/editor
    – Create a JWT token with the editor role: “editor”;
  • /jwt/create/admin
    – Create a JWT token with the administrator role: “admin”;
  • /jwt/roles
    – View the roles in the issued token;
Nginx configuration that calls jwt.js methods

API

The
/security/
route is used as an API application
OPA
Description of roles and methods:
Nginx + OPA

Now it’s Time for Awkward Questions

Is it possible to restrict access to specific resources and not just specific methods?
Partially it is. We have two types of resources: requested and returned. The object that is requested at the time of request can be selected and sent to the authorization request. According to the rules, you need to take into account the parameters of the object. For the returned resource, the response is symmetric. However, the access request will have to be generated after the application has processed the request.
However, that’s not necessary. This implementation is based on the data available in the request with little or no processing. Practically, that’s because token deserialization is quite a significant cost. But it can be reduced by caching tokens at the proxy level. Considering that a token usually lives for more than 15 minutes, this will significantly reduce the processing time.
And if you bring request parsing and extracting key data from the body to the request logic, this can significantly slow down the processing of requests. In the future, however, you will face the fact that the “Give everything available to this user” method will require some post-processing.
Can this approach be used with Graphql or services that ignore REST?
Partially it can. By extracting the Graphql function from the request body, you will manage to more accurately determine the access rights.
However, that’s not necessary. Since this will eventually lead to a loss of performance for the reasons from the first point.

Written by shepelev | Senior Ruby on Rails Developer
Published by HackerNoon on 2022/01/11