Accessly exists out of our need to answer the following questions:
- What can a user do or see?
- Can a user do an arbitrary action on another object?
We were not satisfied with the available resources to answer the questions so we created Accessly!
Accessly is our opinion of access control that can broadly grant permissions to 'actors' (modeled as users
, groups
, organizations
, etc)
Can actor1 view the content resource (/content)?
Our actors
can also have permissions on other models in our application
Can actor1 edit a Post with id 1?
If you have a similar need to implement a permission scheme in your Rails app please continue reading!
Add this line to your application's Gemfile:
gem 'accessly'
And then execute:
$ bundle
Or install it yourself as:
$ gem install accessly
Add the ActiveRecord Migrations:
$ rails g accessly:install
You can use the Accessly gem directly to grant | revoke | check permissions. We recommend the use of 'Policies' covered in this README. Checkout our API docs for more info on using the API directly
We use Accessly with policies in mind to capture everything we want to know about a specific permission set. Let's take a look at some examples:
class ApplicationFeaturePolicy < Accessly::Policy::Base
actions(
view_dashboard: 1,
view_super_secret_page: 2,
view_double_secret_probation_page: 3
)
end
Accessly policies rely on a definition of actions
and/or actions_on_objects
for effective use. This example uses actions
which represent a "permission" that can be granted to an actor for later validation within your app's business logic.
actions
map a symbol to an integer value.- An
action
value should be a unique integer within each policy. - removing/editing
actions
and values can have negative consequences if the underlying data is not migrated
Defined policy actions
become part of the policy API. (see examples below)
With this policy we can grant
permissions to a user
ApplicationFeaturePolicy.new(user).grant!(:view_super_secret_page)
In our SuperSecretPageController
, we can check whether the user has permission to view that page with
ApplicationFeaturePolicy.new(user).view_super_secret_page?
# or
ApplicationFeaturePolicy.new(user).can?(:view_super_secret_page)
At any point in time we can revoke permissions with
ApplicationFeaturePolicy.new(user).revoke!(:view_super_secret_page)
We can grant permissions to actors
on other objects
in our application with a policy like:
class UserPolicy < Accessly::Policy::Base
actions_on_objects(
view: 1,
edit: 2,
destroy: 3
)
def self.namespace
User.name
end
def self.model_scope
User.all
end
end
Accessly policies rely on a definition of actions
and/or actions_on_objects
for effective use. This example uses actions_on_objects
which associate a "permission" with an object in your system. The "object" is typically an ActiveRecord object.
actions_on_objects
map a symbol to an integer value.- An
actions_on_objects
value should be a unique integer within each policy. - removing/editing
actions_on_objects
and values can have negative consequences if the underlying data is not migrated
Defined policy actions_on_objects
become part of the policy API. (see examples below)
We differentiate permissions by a namespace
which by default is the name of your policy class. However,
it may be necessary to override the default behavior represented in the above example.
Accessly can return a relation of ids on an object for a given actor's permission grants. Accessly::Policy::Base
requires
that you implement self.model_scope
with an ActiveRecord
scope so the list
api can return an ActiveRecord::Relation
With this policy we can grant
permissions for a user to do an action on another user object.
UserPolicy.new(user).grant!(:edit, other_user)
In our EditUserController
, we can check permissions
UserPolicy.new(user).edit?(other_user)
# or
UserPolicy.new(user).can?(:edit, other_user)
We can list all of the users available to edit with
UserPolicy.new(user).edit
# or
UserPolicy.new(user).list(:edit)
At any point in time we can revoke permissions with
UserPolicy.new(user).revoke!(:edit, other_user)
Let's look at a policy with a combined configuration and more customization
class UserPolicy < Accessly::Policy::Base
actions(
view: 1,
edit_basic_info: 2,
change_role: 3,
email: 4
)
actions_on_objects(
view: 1,
edit: 2,
destroy: 3,
)
def self.namespace
User.name
end
def self.model_scope
User.all
end
def segment_id
actor.organization_id
end
def unrestricted?
actor.admin?
end
end
This policy combines actions
and actions_on_objects
, introduces Accessly's support for segment_id
, and overrides unrestricted?
Accessly policies can extend support for combined use of actions
and actions_on_objects.
You may want to broadly grant edit_basic_info
permissions to some users. The same policy can support a limited scope of permissions where the actor
and object
must be defined.
segment_id
allows you to scope permission grants to a specific object id that you define. In our example the actor
belongs to an Organization model, and we set the organization_id on each permission granted for any actor using the policy.
It provides additional efficiency on query execution, and we can broadly remove permissions if the organization is no longer in the application.
Accessly uses unrestricted?
to bypass permission checks. This policy shows that the actor has an admin
designation which we do not want to model in permissions. The business logic implemented here would bypass any permission check if unrestricted?
returns true
. When unrestricted?
returns true
, then can?
and the other permission check methods (like edit_basic_info?
in this example) automatically return true
, and list
and the other list methods (like edit
in this example) returns the ActiveRecord::Relation
given by self.model_scope
Let's look at a policy that overrides action?
and list
APIs
class UserPolicy < Accessly::Policy::Base
actions(
view: 1,
edit_basic_info: 2,
change_role: 3,
email: 4
)
actions_on_objects(
view: 1,
edit: 2,
destroy: 3
)
def self.namespace
User.name
end
def self.model_scope
User.all
end
# Override the destroy permission check for an "Action on Object"
def destroy?(object)
if actor.name == "Alice"
true
else
super
end
end
# Override the view permission check for both Action only and "Action on Object"
def view?(object = nil)
if object.nil?
if actor.name == "Bob"
false
else
super
end
elsif actor.name == "Alice" && object.name == "Bob"
true
else
super
end
end
# Override the change_role check for Action only
def change_role?
false
end
# Override the list method for view permissions
def view
if actor.name == "Alice"
User.all
else
super
end
end
end
Here we provide some examples of the Accessly::Policy::Base
overrides you can make in an application. You can override the function completely or fallback to the Base
method. The implementation strategy is up to you!
Any call to the following functions will run the given example in the policy:
# Action on Object queries
UserPolicy.new(user).destroy?(other_user)
# or
UserPolicy.new(user).can?(:destroy, other_user)
# Action queries
UserPolicy.new(user).view?
# or
UserPolicy.new(user).can?(:view)
# Action on Object queries
UserPolicy.new(user).view?(other_user)
# or
UserPolicy.new(user).can?(:view, other_user)
# Action queries
UserPolicy.new(user).change_role?
# or
UserPolicy.new(user).can?(:change_role)
# List queries
UserPolicy.new(user).view
# or
UserPolicy.new(user).list(:view)
Revokes a given permission for all actors and objects.
ApplicationFeaturePolicy.revoke_all!(:view_super_secret_page)
Accessly implements some internal caching to increase the performance of permission queries. If you use the same Policy object for the same lookup twice, then the second one will lookup based on the cached result. Be mindful of caching when using revoke!
or grant!
calls with subsequent permission queries on the same Policy object.
Maintainers:
-
Are active contributors
-
Help set project direction
-
Merge contributions from contributors
If you are interested in contributing, that is exciting! Please check out CONTRIBUTING.md; we appreciate your help!
The gem is available as open source under the terms of the MIT License.