Authorization

Saraki uses an ability based authorization mechanism to determine if a given user can access to an endpoint. This mechanism is composed of resources, actions, abilities, and roles. On each HTTP request, a client must provide an access token with enough privileges (abilities) to perform a given action on a given resource.

Before we start with examples and usage information, let define some concepts and terms:

  • Resource: It is any unit or group of data accessible through an API. To all the resources we want to be protected we assign a single name to them.
  • Action: An action is any type of operation that can be performed on a resource. We must give a name to the action or task that an API endpoint performs. Most of the time it will be one of the classic CRUD operations; create, read, update and delete, but it can be any name, for instance, follow or listen, for a service that propagates information using WebSockets.
  • Ability: The ability to perform an action on a resource. For instance; read products, create products, etc. It is basically just a resource/action pair. But you can add a name and description to it too.
  • Role: A set of one or more abilities. For example, a role Cashier could have the abilities “read payment”, “create payment” or a role Seller can have the abilities “read product”, “read order”, “update order”, “delete order”. A user can have various roles assigned to him.

Saraki uses JSON Web Token and stores the privileges that a user has as a member of given organization in the token payload.

How it works

Assuming we have an endpoint decorated with require_auth(), the way a request is validated against an endpoint happens in this way:

  1. First, look for a valid access token in the incoming request.
  2. Then check if the variable converters match the claims of the current access token.
  3. Finally, check if the scope of the token has the required privileges defined in require_auth().

If any of those steps fail, the application won’t execute the view function and will respond with 401 Unauthorized status code.

Authorization rules

The way we define authorization rules on a view function is passing the name of the resource and the action required to the require_auth() decorator.

The require_auth() decorator plays an important role here because it collects all resources and actions used by the application to latter save then in the database.

Take into account the next code:

@app.route("/products")
@require_auth("product")
def list_products():
    return []

In the above code, we define that a token must contain the product resource explicitly and the action read implicitly. By implicitly we mean that if an action name is not provided, the actual route rule HTTP method (GET in this case) will be mapped to a predefined action (read in this case). So an access token with the next payload would be able to perform a GET request to the above-defined endpoint.

{
    "sub":"coyote",
    "scp": {
        "product": ["read"]
    },
}

Here the list of predefined action/method mapping:

Method Action
GET read
POST write
PATCH write
DELETE delete

Let’s see three more examples to fully understand how this work:

@app.route("/products", method=["POST"])
@require_auth("product")
def add_product():
    pass

@app.route("/products/:id", method=["PATCH"])
@require_auth("product", "update")
def update_product():
    pass

@app.route("/products:/id", method=["DELETE"])
@require_auth("product")
def delete_product():
    pass
  1. The first view function requires an access token with the scope "product": ["write"]. The required action is write because the method to which the route listen is POST.
  2. The second view function passes a custom action name update, so it will require a scope equal to "product": ["update"]. Note that the required action is update and not write anymore.
  3. And the last one requires "product": ["delete"] because the HTTP method is DELETE.

The next access token scope should be able to perform a request to any of the three defined endpoints above:

{
    "sub":"coyote",
    "scp": {
        "product": ["read", "write", "update", "delete"]
    },
}

Variable Converters

Another way of adding authorization constraints are the route rule variable converters. They are very important because they will help the application segregate the data access between tenant in the database. Currently, there are two converts:

converter value
sub username. The user account.
aud orgname. The organization account.

When one of those variable converters appears in a route rule, the authorization mechanism will ensure that the current access token claims match the variable values of the current URL.

Suppose we have a view function with the route rule /users/<sub:username>/activity, and an incoming request to /users/coyote/activity. For the request to be successful the access token must have the sub claim with the value coyote.

{"sub":"coyote"}

If the request is successful, the local proxy current_user is available. This object always points to the user performing the current request.

The aud converter works in exactly the same way, there is no difference. Let’s use both of them in a single route rule:

from saraki.auth import current_org, current_user

@app.route("/orgs/<aud:organame>/members/<sub:username>/activity")
@require_auth()
def index(organame, username):
    # your code here

In the above code we imported current_org which will point to the current organization being accessed.

A request to /orgs/acme/users/coyote/activity must have a token with the next payload:

{"aud": "acme", "sub":"coyote"}

The local proxies current_org and current_user must be used to ensure that operations to the database are made on the correct organization and user account. So organizations do not end up reading or modifying data from other organizations.

Access token

Currently, the only supported token format is JSON Web Token. You are going to find a lot of documentation about JWT on the internet, so we are not going to cover the specification here.

There are two types of access token:

  1. User access token: This token give access to protected endpoints which aren’t tenant endpoints. It also gives access to endpoints which handles user-specific data. These type of endpoints usually has the sub converter.
  2. Org access token: Gives access to tenant-specific endpoints. Those are endpoints which have the aud converter.

A JSON Token transport key/value pairs as payload. Here a list of important claims that you should be aware of:

  • sub: This is the username to which a token belongs. This is always present.
  • aud: This is the organization to which this token has access. What this means is that a token that belongs to an organization can not access endpoints that belong to other organizations.
  • scp: This is the scope in which a token can operate. It stores the privileges of a user in a dictionary. The properties are the resources and the values are a list of actions that can be performed on the resource.

Here a JWT payload that illustrates with the three claims above listed.

{
    "aud": "acme",
    "sub":"coyote",
    "scp": {
        "catalog": ["read"],
        "sale": ["read", "write", "delete"]
    }
}