Resource-level Enforcement

Is the current user allowed to perform a certain action on a certain resource? This is the central question of “resource-level” enforcement.

  • Can a user update settings for this organization?
  • Can a user read this repository?
  • Can an admin resend a password reset email for this user?

Resource-level enforcement is the bread-and-butter of application authorization. If you only perform one type of authorization in your app, it should be this. Just about every endpoint in your application should perform some kind of resource-level enforcement.

Authorize an action

The method to use for resource-level authorization is called Oso.authorize. You use this method to ensure that a user has permission to perform a particular action on a particular resource. The authorize method takes three arguments, user, action, and resource. It doesn’t return anything when the action is allowed, but throws an error when it is not. To handle this error, see Authorization Failure.

oso.authorize(user, "approve", expense)

The authorize method checks all of the allow rules in your policy and ensures that there is an allow rule that applies to the given user, action, and resource, like this:

allow(user: User, "approve", _expense: Expense) if
    user.is_admin;

Let’s see an example of authorize from within an endpoint:

def approve_expense(user, expense_id):
    expense = db.fetch(
        "SELECT * FROM expenses WHERE id = %", expense_id)
    oso.authorize(user, "approve", expense)

    # ... process request

As you can see from this example, it’s common to have to fetch some data before performing authorization. To perform resource-level authorization, you normally need to have the resource loaded!

Authorization Failure

What happens when the authorization fails? That is, what if there is not an allow rule that gives the user permission to perform the action on the resource? In that case, the authorize method will raise an AuthorizationError. There are actually two types of authorization errors, depending on the situation.

  • NotFoundError errors are for situations where the user should not even know that a particular resource exists. That is, the user does not have "read" permission on the resource. You should handle these errors by showing the user a 404 “Not Found” error.
  • ForbiddenError errors are raised when the user knows that a resource exists (i.e. when they have permission to "read" the resource), but they are not allowed to perform the given action. You should handle these errors by showing the user a 403 “Forbidden” error.

Note: a call to authorize with a "read" action will never raise a ForbiddenError error, only NotFoundError errors—if the user is not allowed to read the resource, the server should act as though it doesn’t exist.

You could handle these errors at each place you call authorize, but that would mean a lot of error handling. We recommend handling NotFoundError and ForbiddenError errors globally in your application, using middleware or something similar. Ideally, you can perform resource-level authorization by adding a single line of code to each endpoint.

As an example, here’s what a global error handler looks like in a Flask app:

from oso import ForbiddenError, NotFoundError

app = Flask()

@app.errorhandler(ForbiddenError)
def handle_forbidden(*_):
    return {"message": "Forbidden"}, 403

@app.errorhandler(NotFoundError)
def handle_not_found(*_):
    return {"message": "Not Found"}, 404

Then, when your application calls authorize, it will know how to handle errors that arise.

Connect with us on Slack

If you have any questions, or just want to talk something through, jump into Slack. An Oso engineer or one of the thousands of developers in the growing community will be happy to help.