AccessAllow
Permissions and access control gem for Rails.
Roles, Abilities and the permissions model
Users should be assigned a role
where a role is a named grouping of specific permissions (or abilities as we
call them). Roles are configured in the application configuration.
Abilities are named permissions that live inside a namespace. These are context dependant. For example we might think
of the ability for being able to check out a shopping cart as shopping_cart: :check_out
where shopping_cart
is the
ability namespace for anything to do with the shopping cart and check_out
is the specific ability name.
Thus abilities are acquired by user either through their assigned role, or an ability can be directly assign in the
database, via the User association permissions
.
Role and abilities utility methods
AccessAllow::Roles
provides a bunch of utility methods that
- check if a given role name is for a specific user type or not
- returns humanized versions of the role names
AccessAllow::Abilities
currently provides utility methods to convert between string and hash representations of abilities.
Configuring Roles & their abilities
Schema configured in Configuration
and should be configured to create roles with their abilities.
The structure consists of:
<user_type_key>:
<user_role_key>:
<ability_namespace_key>:
<ability_key>: [true/false]
where
user_type_key
is determined from the model name of the user class (eg User
=> user
)user_role_key
is the name of the user role (eg account_owner
)ability_namespace_key
is the name of the group of abilites (eg product_management
)ability_key
is the name of the actual ability (eg edit_product
) and is set to a boolean
to indicate if the ability is available to the specific configuration or not
Note: ability names must be defined in the correct user type, role, namespace key space otherwise
the app will raise an exception. This is to prevent accidentally forgetting to define the default
permissions of a role around a specific feature.
Setting a user specific ability
User specific abilities are persisted in Permission
s where the attribute ability_name
stores the
ability namespace and name in one combined string. The format is /
separated. Eg tag_management/edit_tag
. Use
AccessAllow::Abilities
to convert between string and hash representations of abilities.
The existence of a Permission
sets the specific ability in the above described structure of abilities.
The AbilitiesManager
handles mixing these assigned abilities into the users specific total ability list.
Note that an ability defined in a Permission
must also exist in the role assigned abilities. If
it does not then it is ignored. In other words a Permission
can only override abilities defined for the
role that are set to false
. This allows a user to be given a specific ability that normally their role has not got,
but does not allow you to assign arbitrary abilities to a user, thus preventing dangerous situations where an ability
that say is only for Admins is assigned to a User role.
Manually checking user abilities
The class AccessAllow::Check
implements ability check logic. Using this class one can check if a user has a specific ability
and optionally raise if not.
You can either build a new instance of the check class and then use #possible?
and #possible!
of use the class
helper methods
.call(user, <ability_namespace>: <ability_name>)
: checks if user has ability_name
in ability_namespace
. Returns
a boolean result.call!(user, <ability_namespace>: <ability_name>)
: checks if user has ability_name
in ability_namespace
. Returns
true or raises AccessAllow::ViolationError
The methods exposed by Check
are useful for checking for abilities in other objects. To define abilities checks
around controller actions see the next section.
Controller DSL for specifying requirements and abilities needed to perform actions
Much of the time permissions checks will occur in Controllers. Also many controller actions have specific checks and
requirements around the user or other entities related to the controller action. For example, when editing a user's
profile, one must check that the user who is trying to execute the update
action has the ability (permission) to
do it, but also that the user is even from the same company as the user being edited.
As such a DSL exists that can be used in controllers to define sets of required checks and rules around actions that then
define what abilities or checks are needed to allow a specific action to execute. The rules can also define what
should happen if the checks do not pass, or if no rules match the current situation.
The DSL allows us to define 3 categories of our so called access rules:
Required check rules
Many times we want to specify that certain requirements are required to allow a user to perform a certain action. These
requirements maybe certain checks on the user, or they maybe related to their role or abilities.
These checks must all pass to allow the user to continue. They are checked before any other access checks are executed.
If the checks do not pass then a 'violation' is returned, which is then handled by the controller accordingly.
These rules consist of a 'check', an optional set of required abilities, and optionally what violation type to raised
if the check does not pass.
Action allow rules
Action allow rules are defined to provide specific rules which allow a user to perform a specific controller action.
Note that AccessAllow prevents an action from being executed unless it is explicitly allowed for the given user trying to
execute it.
For a user to be allowed to perform a given controller action, there must be a matching "action allow" rule for that
action for which the check passes and permissions requirements are met. Any matching rule will allow the user to execute
the action. Note if no rules match successfully then the no-match behaviour is executed.
These rules consist of a check, an optional set of required abilities, a set of action names to which the rule applies
and optionally a name to alias the check as a "named check" rule (see below).
Named check rule
These are named checks that can then be referenced by name in action logic or in views to say perform some conditional
logic. The method provided for checking if any named check rule is valid for the current context is access_allowed?
.
There is more details below on this.
Say for example you want a user to be approved
and have the ability company_profile/edit
to edit the company
profile, and want to conditionally display a "Edit" button in the view. You could define a named check, say
:approved_can_edit
(that checks approved
and that the user has the ability) and use it in the view to conditionally
display the button:
<% if access_allowed? :approved_can_edit %>
The button...
<% end %>
Note that when you define an "action allow" rule it is automatically also added to these 'named checks' by the action
name, for example, if there is action allow rule for :create
then we can use access_allowed? :create
.
Also note that it is possible to specify a custom 'named check' name for the "action allow" rule (see more below).
No-match behaviour
The DSL also allows us to define what should happen when executing an action and no rule matches the current situation.
What should happen is defined using one of a set of predefined 'violations' which are handled in specific ways. See
the discussion below on violations.
Violations
The behaviour when a "required check" or an action has no matching "allow rule" is defined with so called "violation"
configuration. These violations are handled in a standardised way by the controller callback that performs the rule
checks.
The violation types are:
severe
:
this violation is considered something unusual and is logged. The end user will simply see a 404 page to avoid exposing
to them that there is in fact an actionable endpoint at the route they tried to access.hidden
:
this violation type is considered less severe, but still aims to avoid leaking information to the end user
about the actual available routes on the app. If this violation is raised the user will see a 404 page and
the violation is logged to the app logs.not_permitted
:
this violation is used when a user can know that an action and route exists but that they do not
have the assigned 'abilities' to perform the action. The end user will see a 403 (forbidden) page
and the violation is simply logged to the app logs.redirect
:
this violation type is used when we want to perform a redirect if the user does not have the necessary
permissions. By default it will redirect to root_path
but you can use a block to specify the destination path.
The block must return a string or other structure that is accepted by redirect_to
.
The DSL & defining checks
The methods are as follows:
access_require(check, with:, violation: :severe, &block)
Used to define a "required check rule".
Takes a check name (a symbol or array of symbols) (see details below), an optional violation
type (defaults to
severe
) for when the check does not pass, and a block for when the violation type is redirect
and you want to
specify custom logic to determine the redirection destination. Also can take an an optional set of abilities (a hash)
passed to with:
to check against the user.
access_allow(check, with: nil, to:, as: nil)
Used to define an "action allow rule" with optional named check alias.
Takes a check name (a symbol or array of symbols) (see details below), an optional set of abilities (a hash) passed to
with:
to check against the user, and an optional name (symbol) passed with as:
to allow the rule to be used as a
"named check". The controller actions the rule applies to is passed to to:
(symbol or array of symbols).
access_allow(check, with: nil, as:)
Used to define a "named check rule".
Similar to the "action allow rule" but without the actions. This rule is thus only available to be used as a "named
check".
access_no_match(violation, &block)
Used to define the "no match" behaviour, ie what happens when an action is trying to be executed by no access rule
matches or passes for the given user and action.
Takes a violation
type and optionally a block for when the violation type is redirect
and you want to specify
custom logic to determine the redirection destination.
Defining abilities needed
Permissions requirements are specified for the rule with with:
.
The permissions are defined as a hash containing keys representing the ability namespaces and associated values
representing the required abilities.
For example, {tag_management: [:add_new, :edit_existing], product_management: :edit_variants}
would mean that the
user must have all 3 of the abilities, tag_management: :add_new
, tag_management: :edit_existing
and
product_management: :edit_variants
.
Defining Checks & predefined checks
Access rules must specify one or more 'checks' as part of their rule definition.
'Checks' are basically controller methods which return a boolean to determine if the check 'passed' or 'failed'. Checks
are normally custom code written for the given context of the feature. Note that checks do not need to perform the
abilities checks specified by with:
, these are performed by the gem logic for you.
Checks are specified by providing an instance method on the controller named allow_(name)?
, where name
is the check
name, and which returns a boolean.
For example, if defining a check for an action allow rule where the user must be approved on the platform, and
have a specific ability assigned to them, then the 'check' part (named say approved_user
) is "user must be approved
on the platform" part of the rule, and would be defined on the controller as an instance method allow_approved_user?
.
There are some predefined 'common' checks, where you do not need to define the allow_(name)?
method. These are:
:public
: anyone, logged in or not:authenticated_user
: any logged in user (uses current_user
or whatever is set as the current_user_method
in the config)
View helper to check permissions of user for conditional view sections
It is also possible to check access
rules from inside views using the access_allowed?
view helper, which takes
a list of "named check" names. If any of those check names passes the method returns true
.
Note that check names also include the actions for which rules exists, as described earlier.
# in controller
allow_access :admin, to: :new
# in view
<% if access_allowed? :new %>
Only 'admin' users who are allowed to execute action `:new` can see this
<% end %>
and
# in controller
allow_access :my_check, as: named_rule
# in view
<% if access_allowed? :named_rule %>
Only users for whom the `named_rule` check passes can see this
<% end %>
Example
Consider the following view fragment, and then the controller heirarchy defined below:
tags_controller.rb
class TagsController < AdminController
access_allow :admin, to: [:index, :show]
access_allow :admin, with: {tag_management: :manage}, to: :all, as: :tag_management
access_allow :admin, with: {tag_management: :usage_stats}, as: :view_usage_stats
access_allow :magic_admin, to: :magic
def allow_admin?
current_user.admin?
end
def allow_magic_admin?
current_user.im_magic? && allow_admin?
end
end
class AdminController < AuthenticatedController
access_require :admin, violation: :severe
access_no_match :not_permitted
end
class AuthenticatedController < ApplicationController
access_require :authenticated_user, violation: :redirect do
sign_in_path
end
end
class ApplicationController < ActionController::Base
access_no_match :hidden
end
tags/index.html.erb
<p>Tags Index</p>
<% if access_allowed? :tag_management %>
<button>Add new tag</button>
<% end %>
<% if access_allowed? :view_usage_stats %>
<div> ... </div>
<% end %>
<ul> ... </ul>
Usage
Add this to your ApplicationController
class ApplicationController < ActionController::Base
include AccessAllow::ControllerAccessDsl
end
Installation
Add this line to your application's Gemfile:
gem "access_allow"
And then execute:
$ bundle
Or install it yourself as:
$ gem install access_allow
Then run the generator to add the initializer
rails g access_allow:install
Contributing
Contribution directions go here.
License
The gem is available as open source under the terms of the MIT License.