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.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
end
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 Oso::AuthorizationError
. There are actually two types of authorization
errors, depending on the situation.
Oso::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.Oso::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 Rails app:
class ApplicationController < ActionController::Base
rescue_from Oso::ForbiddenError, with: :forbidden_error
rescue_from Oso::NotFoundError, with: :not_found
def forbidden_error
render 'forbidden', status: 403
end
def not_found_error
render 'not found', status: 404
end
end
Then, when your controllers call authorize
, they don’t need to worry about
handling the errors.
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.