Authorization
Where should permissions be checked?
When deciding where to check permissions, apply defense-in-depth by implementing multiple checks at different layers. Starting with low-level layers, such as finders and services, followed by high-level layers, such as GraphQL, public REST API, and controllers.
For more information, see guidelines for reusing abstractions.
Protecting the same resources at many points means that if one layer of defense is compromised or missing, customer data is still protected by the additional layers.
For more information on permissions, see the permissions section in the secure coding guidelines.
Considerations
Services or finders are appropriate locations because:
- Multiple endpoints share services or finders so downstream logic is more likely to be re-used.
- Sometimes authorization logic must be incorporated in DB queries to filter records.
- You should avoid permission checks at the display layer except to provide better UX, and not as a security check. For example, showing and hiding non-data elements like buttons.
The downsides to defense-in-depth are:
-
DeclarativePolicy
rules are relatively performant, but conditions may perform database calls. - Higher maintenance costs.
Exceptions
Developers can choose to do authorization in only a single area after weighing the risks and drawbacks for their specific case.
Prefer domain logic (services or finders) as the source of truth when making exceptions.
Logic, like backend worker logic, might not need authorization based on the current user.
If the service or finder's constructor does not expect current_user
, then it typically does not
check permissions.
Frontend
When using an ability check in UI elements, make sure to also use an ability check for the underlying backend code, if there is any. This ensures there is absolutely no way to use the feature until the user has proper access.
If the UI element is HAML, you can use embedded Ruby to check if
Ability.allowed?(user, action, subject)
.
If the UI element is JavaScript or Vue, use the push_frontend_ability
method,
which is available to all controllers that inherit from ApplicationController
.
You can use this method to expose the ability, for example:
before_action do
push_frontend_ability(ability: :read_project, resource: @project, user: current_user)
end
You can then check the state of the ability in JavaScript as follows:
if ( gon.abilities.readProject ) {
// ...
}
The name of the ability in JavaScript is always camelCase,
so checking for gon.abilities.read_project
would not work.
To check for an ability in a Vue template, see the developer documentation for access abilities in Vue.
Tips
If a class accepts current_user
, then it may be responsible for authorization.
Example: Adding a new API endpoint
By default, we authorize at the endpoint. Checking an existing ability may make sense; if not, then we probably need to add one.
As an aside, most endpoints can be cleanly categorized as a CRUD (create, read, update, destroy) action on a resource. The services and abilities follow suit, which is why many are named like Projects::CreateService
or :read_project
.
Say, for example, we extract the whole endpoint into a service. The can?
check will now be in the service. Say the service reuses an existing finder, which we are modifying for our purposes. Should we make the finder check an ability?
- If the finder does not accept
current_user
, and therefore does not check permissions, then probably no. - If the finder accepts
current_user
, and does not check permissions, then you should double-check other usages of the finder, and you might consider adding authorization. - If the finder accepts
current_user
, and already checks permissions, then either we need to add our case, or the existing checks are appropriate.