API Authorization using Aserto and Zuplo

Sep 25th, 2024

Omri Gazitt avatar

Omri Gazitt

API Authorization  |  

Topaz  |  

Open Policy Agent  |  

Authorization  |  

Security

aserto and zuplo

API Authorization

Consistent API management is emerging as one of the most important functions of a Platform Engineering team. With projects like Backstage, these teams have a way to expose and document their API estate.

Authorizing access to APIs is the next step in this journey. With Aserto’s API Authorization solution, onboarding new services is as simple as importing their OpenAPI definition and assigning users or groups various levels of access to those services, or to individual endpoints.

Enforcing authorization using an API gateway

Since Platform Engineering teams don’t control the code to every API, they often look for common control points. An API gateway is the perfect enforcement point for an authorization policy, since it can filter requests before they are forwarded to the service, thereby requiring zero changes to the API code.

Zuplo is a modern API gateway company, and we now have a first-class integration between Aserto and Zuplo. Check out this webinar, where Josh Swift and I discuss the partnership, and demonstrate how easy it is to add fine-grained authorization to your APIs.

Or if you'd like to try it out for yourself, follow this guide to use Aserto and Zuplo together to secure your APIs.

A step-by-step guide

For expediency, this walkthrough uses the Aserto hosted authorizer as the policy decision point that is called by the API gateway. In a production environment, you’d change the authorizer service endpoint to a Topaz instance that is running in your cluster, and connect that Topaz instance to the Aserto control plane. Everything else is basically the same.

With that said, let’s jump in!

Instantiate the API Authorization template

Go to the Aserto Console, and if you don’t have an account yet, sign up for a free account. Once you’ve verified it, pick an account name, and select the API Authorization template.

If you already have an Aserto account, you can install the API Authorization template here.

apply template

This results in the creation of a new policy instance named api-auth.

Your tenant now has the API Authorization model, which includes the user, group, service, and endpoint object types, and the relations and permissions on these types.

Finally, the template also loaded some sample users and groups, modeled after the Rick and Morty cartoon, as well as three sample services - Petstore API, Rick and Morty API, and Todo API.

Import an OpenAPI definition

Click on the api-auth policy instance, and select the Quickstart tab. The Citadel identity provider that contains the Rick and Morty users is already connected. In the next step, you can optionally import your own OpenAPI spec. If you have one ready, try it out… otherwise feel free to skip this step and rely on the existing three services that we’re using as sample data.

import openapi

The OPA policy

Click on the Modules tab, and observe a single Rego module - the boilerplate policy-rebac.check module.

check module

Compare that with the complexity of having to manage a custom policy for every service! As mentioned, we are transforming a policy problem into a data modeling problem, which we will explore next.

But it’s important to note that any additional attribute-based access control or environment-oriented access restrictions can be easily added to this boilerplate policy. For example, you can easily extend the policy to ensure that users who are “contractors” are only allowed to invoke endpoints on weekdays.

Since every OPA policy is a Topaz policy, you can bring the full power of OPA and Rego to bear on your custom authorization policies.

The API Authorization model

Click the Directory tab, and the Edit manifest button. This will show you the API Authorization model definition. The important type definitions are for service and endpoint.

api-auth authorization model

types:
  # user represents a user that can be granted role(s)
  user:
    relations:
      manager: user

    permissions:
      ### display_name: user#in_management_chain ###
      in_management_chain: manager | manager->in_management_chain

  # group represents a collection of users and/or (nested) groups
  group:
    relations:
      member: user | group#member

  # identity represents a collection of identities for users
  identity:
    relations:
      identifier: user

  # service represents a set of endpoints
  service:
    relations:
      owner: user
      deleter: user | group#member
      creator: user | group#member
      writer: user | group#member
      reader: user | group#member

    permissions:
      can_get: reader | can_put
      can_put: writer | can_post
      can_patch: writer | can_post
      can_post: creator | can_delete
      can_delete: deleter | owner

  # endpoint represents a specific API endpoint
  endpoint:
    relations:
      # each endpoint picks the reader/writer/creator/deleter relation to the service
      # based on the method (GET -> reader, PUT/PATCH -> writer, etc)
      service-reader: service
      service-writer: service
      service-creator: service
      service-deleter: service
      # invoker allows a user or group to get access to invoke this specific endpoint
      invoker: user | group#member
    permissions:
      can_invoke: invoker | service-reader->can_get | service-writer->can_put |
        service-creator->can_post | service-deleter->can_delete

Let’s start at the bottom. An endpoint has a can_invoke permission, which is directly assignable by creating an invoker relationship to a user or a group. This allows an API administrator to entitle a user or a group directly on a discrete endpoint.

The endpoint also has relations called service-reader, service-writer, service-creator, and service-deleter which ladder up to the enclosing service. The default transformation that occurs when importing an OpenAPI definition is to set the relationship of the endpoint based on its HTTP method - a GET creates the service-reader relation, a PUT or PATCH creates the service-writer relation, a POST uses the service-creator relation, and a DELETE uses the service-deleter relation. This allows the can_invoke permission to ALSO be assignable via relationships that a user or group has to the service. You can of course customize this default transform if you have different conventions or needs.

Now, let’s look at the service type. A service has discrete permissions called can_get, can_put, can_patch, can_post, and can_delete which are assignable through the reader, writer, creator, deleter, and owner relations on the service. These permissions are additive, in the sense that a deleter can invoke DELETE endpoints, and can also do anything that a creator can do. A creator can POST, and can also do anything that a writer can do… and so on.

To put it all together, users (or groups) can be entitled at the level of an entire service, at the level of a class of endpoints on a service (e.g. all GET endpoints), or at the level of a discrete endpoint. This provides a lot of flexibility in API entitlement, while keeping things simple and consistent.

Next, let’s look at the user, group, service, and endpoint instance data.

Authorization data

Click on the Objects tab, and within that the Service type. You should see the three services that were automatically added by the template.

api auth services

Let’s follow the trail of entitlements from users and groups to the services and endpoints. Click on the User type, which should show the five Citadel users - Beth, Jerry, Morty, Rick, and Summer.

Let’s click on Rick. As you can see, Rick is a member of the Global Deleters group.

api auth rick

Next, click on the Global Deleters group.

global deleters group

This group aggregates the deleters group for each service. This pattern makes Rick a super-user - he can invoke any endpoint in the system, since the members of the Petstore API Deleters, Rick and Morty API Deleters, and Todo API Deleters groups can invoke any endpoint on the respective services, and being a member of the Global Deleters group means that Rick is transitively a member of these groups.

Next, let’s look at Morty - click the User type and then click Morty. Morty is a member of the Petstore API Creators group, which means he can invoke any Petstore endpoint that is not a DELETE. This demonstrates the pattern of how to entitle a user on a set of methods within a service.

api auth morty

Finally, let’s go back to the Service type and click the Todo List API. This shows the six endpoints that are part of this API.

todo service

The group called Todo List API Readers is a reader of the service, meaning every member is entitled to invoke all the GET endpoints on this service. Click on the Todo List API Readers, and follow the trail of nested groups. As you can see, it includes the viewer-group, which comes from the Citadel IDP.

todo readers

Clicking the viewer-group reveals its members - Beth and Jerry, as well as the members of the editor-group. Clicking the editor-group reveals its members -  Morty and Summer, as well as members of the admin-group. And the admin-group includes Rick. So, transitively, every one of our protagonists are entitled to invoke any GET endpoint on the Todo API Service.

This pattern shows how to use nested groups in the IDP to control entitlements to classes of endpoints (in this case, the GET endpoints) in a service.

Integrating authorization with an API gateway

Let’s use Zuplo as an example of a modern API gateway. Create a free account, and use their Todo sample template. This should result in something like this:

zuplo

Add inbound policies to the routes

Next, click the routes.oas.json file on the left navbar, and click the GET /v1/todos endpoint. Open the Policies chevron.

zuplo policies

Add a new policy called API Key Authentication:

api inbound policy

Add another below it using the policy type Aserto Authorization.

aserto authorization policy

You can call the Aserto Authorization policy aserto-authz-inbound, and configure it using the values shown below.

aserto authz inbound

{
  "export": "AsertoAuthZInboundPolicy",
  "module": "$import(@zuplo/runtime)",
  "options": {
    "tenantId": "tenant-id",
    "authorizerApiKey": "authorizer-api-key",
    "policyName": "api-auth",
    "serviceName": "todo"
    "userSubPropertyPath": ".data.email"
  }
}

Note that the values for tenantId and authorizerApiKey should come from the Settings tab of the api-auth policy instance in the Aserto Console.

policy settings

You can repeat this process for all six routes, but after you’ve done it once, you can just select the policies you’ve already selected from the top of the policy selector (in other words, you only have to create and configure the aserto-authz-inbound once).

zuplo policies

Now you can save your work by pressing command-S.

Set up API keys

Lastly, you’ll need to set up API Key consumers. Click the Services tab and the API Key Service.

api key service

Create a consumer for each of Rick and Morty:

morty api key

Ensure that the metadata for Rick is the following:

{
  "sub": "CiRmZDA2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs",
  "email": "rick@the-citadel.com"
}

The metadata for Morty has a slightly different sub claim and Morty's email:

{
  "sub": "CiRmZDE2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs",
  "email": "morty@the-citadel.com"
}

Now we have the two users set up, which we can use in the Authorization header of the requests that we send the gateway. The API key inbound policy will check that the user has a valid API key, and make it available in the aserto-authz-inbound policy. This will be the subject that the Aserto policy will pass to the Aserto hosted authorization service, along with the tenant ID, API key, policy name, and service name that we configured above. The subject passed into the Aserto authorizer will come from the .data.email field that we placed in the API key metadata.

Make sure you copy the API keys associated with Rick and with Morty by using the "copy handles" next to each one - we’ll use them in the next section.

api keys

Testing the APIs

We can finally test our APIs using Zuplo’s Test modal. Go back to the routes.oas.json file in the left navbar, click the DELETE /v1/todos/{todoId} route, and click the Test button.

routes test modal

Now, fill in an arbitrary value for the todo ID, and enter the Authorization header and its value:

test modal authz headers

In the screenshot you’ll see that we also put in some dummy headers - AuthorizationRick and AuthorizationMorty, just so we can copy-paste these headers (and their Bearer tokens) into the Authorization header value. This is a convenient way to flip between invoking the service as Rick and as Morty.

First, let’s try as Rick. Copy and paste the value of the AuthorizationRick header into the Authorization header value. Then click the Test button. You should see an HTTP 200 OK status, and the logs should show that Aserto returned the resulting allowed decision as true. This is as expected, since Rick is a superuser and can invoke all endpoints by virtue of being a Global Deleter.

test as rick

Next, paste Morty’s bearer token into the Authorization header, and click Test. You should see an HTTP 403 Forbidden status, as expected, since Morty is a member of the viewer-group, which can only invoke the GET APIs on the Todo API service.

test as morty

Break the glass

Finally, let’s simulate a “break the glass” scenario where Morty needs access to the DELETE /v1/todos/{todoId} endpoint.

Go to the Aserto Console, click the Directory tab, and the Endpoint type. Type “delete” in the Find input and click the todo:DELETE:/v1/todos/{todoId} endpoint.

find delete endpoint

Click the Outgoing relations tab, and the invoker relation:

invoker relation for morty

Click the Add a relation button and select User for the type and Morty for the instance. Click Add relation and you should see Morty as a direct assignee of the invoker relation.

add relation modal

Go back to the Zuplo console, and click the Test button again. You should now see an HTTP 200 OK status code, indicating that Morty is now able to invoke the DELETE /v1/todos/{todoId} endpoint. If you go back to the Aserto Console and delete the relation you just added, Morty will once again receive an HTTP 403 Forbidden status.

Governance

Before we wrap up, it’s worth noting that with the ReBAC model, we can now trivially find out which users are able to invoke which endpoints.

Go back to the Aserto Console, click on the Directory tab, and the Evaluator. Select the request called “Find objects that a user can access”, and select Beth, the Endpoint type, and the can_invoke permission.

find endpoints for beth

Clicking the “Play” button invokes the query, and shows that Beth can only invoke the three GET endpoints on the Todo API service.

beth endpoints

This makes sense because she’s a member of the viewer-group, and doesn’t have any additional entitlements. Play around with other users such as Morty and Rick to get different results, and feel free to copy the request as a cURL and execute it from a terminal to see how to invoke this type of request as a REST call.

curl 'https://directory.prod.aserto.com/api/v3/directory/graph/endpoint/can_invoke/user?object_id=&subject_id=beth%40the-smiths.com' \
          -H 'aserto-tenant-id: <your-tenant-id>' \
          -H 'authorization: basic <your-dir-api-key>' \
          -H 'content-type: application/json'

Lastly, we can ask the question in the opposite direction - which users can invoke an endpoint? Select the “Find users that can access an object” request, select the first endpoint (DELETE /v1/todos/{todoId}), and select the can_invoke permission. You should see only a single user - Rick - that is entitled to invoke that endpoint.

users for endpoint

Summary

This tutorial covered a lot of ground - the API Authorization model, importing an OpenAPI spec, entitling users on services and endpoints, calling Aserto / Topaz from an API gateway, and answering governance questions.

This is only the tip of the iceberg, but should hopefully show the potential of setting up a scalable way to perform fine-grained API authorization for all your services, enforced at the API gateway.

Aserto has a CLI toolchain that enables organizations to easily onboard a new service from its OpenAPI spec, creating the possibility of a CI/CD pipeline that easily adds new services (or handles adding new endpoints) in an automated fashion.

Entitling users to endpoints can be done by adding users to IDP groups (which are imported into Aserto), and break-the-glass scenarios can be achieved in the Aserto Console, through the aserto/topaz CLI, through the REST or gRPC APIs, or one of our language SDKs.

We hope you enjoyed this tutorial. We have a longer-form video version here, and if you have any questions, feel free to contact us or join our community slack.

Happy hacking!

Omri Gazitt avatar

Omri Gazitt

CEO, Aserto