Recurso
Recurso is a gem designed to make complicated permissions systems a breeze.
It uses a 'permission' model to relate 'identities' (in most cases, your users), with various related 'resources' within your app.
It offers a simple, performant way to manage complex or cascading permissions
Installation
Add this line to your application's Gemfile:
gem 'recurso'
And then execute:
$ bundle
Or install it yourself as:
$ gem install recurso
Usage
1. Specify the 'identity' class
Include the concern in the class represented by your users (usually User):
# user.rb
include Recurso::Identity
2. Specify the 'resource' classes
Next, we'll specify which classes those users may gain access to by adding the Recurso::Resource
concern:
class Organization
include Recurso::Resource
has_many :teams
end
class Team
include Recurso::Resource
belongs_to :organization
has_many :squads
end
class Squad
include Recurso::Resource
belongs_to :team
end
3. Using policy classes
Now, we'll be able to combine the two via a 'policy' method, which will allow us to ask questions about what permissions the user has on any given resource.
These policy classes can be used in two ways; the first to ask whether a user can perform an action on a given resource:
@user.policy(@organization).view?
# ^^ can I view the given organization?
@user.policy(@team).modify?
# ^^ can I modify the given team?
@user.policy(@squad).administer?
# ^^ can I administer the given squad?
The second, which resources a user can access of a given relation:
@user.policy(@organization).(:teams)
# ^^ which teams can I view within this organization?
@user.policy(@team).(:squads)
# ^^ which squad can I view within this team?
Calling resources_with_permission
directly on an identity will return all records in the database which that identity has access to.
(NB that these classes must be whitelisted via the global_relations
config parameter, documented below)
@user.(:teams)
# ^^ which teams in the the database can I view?
4. Authorizing resources
A common use case for these permissions is to authorize actions on a certain controller action.
Recurso provides a controller helper method to make this easy!
# teams_controller.rb
include Recurso::Controller
def show
@team, :view?
end
def update
@team, :modify?
end
def destroy
@team, :administer?
end
if an authorization fails, Recurso will throw a Recurso::Forbidden
error, which you can handle as you see fit:
rescue_from(Recurso::Forbidden) { render json: { error: :forbidden }, status: 403 }
The Recurso::Controller
module also includes a policy
shorthand method, which allows for easy permission checking.
include Recurso::Controller
private
# required: define a default identity, like the currently logged in user
def default_policy_identity
current_user
end
# optional: define a default resource, like the current resource (defaults to nil)
def default_policy_resource
current_resource
end
This will be included as a helper method if included into a controller, so you can use it in the view as well:
<%= if policy(@squad).modify? %>
<button>Edit squad</button>
<% end %>
if no resource is passed, the default_policy_resource
will be used
def default_policy_resource
@squad
end
assert policy.modify? == policy(@squad).modify?
5. Enabling cascading permissions
One of the more powerful features of Recurso is to allow permissions to cascade between resources. So, for instance, if a user has administer access to a Team
, they will also have administer access to all Squad
s within that team.
In order to set this up, we need to defined the relevant_association_names
on a resource
class Squad
include Recurso::Resource
belongs_to :team
has_one :organization, through: :team
def relevant_association_names
[:itself, :team, :organization]
end
end
^^ NB the use of the special association :itself
there, which specifies that we should look to see if the user has permission to the Squad, in addition to its team and organization.
Now, calling the view?
, modify?
, or administer?
methods on a squad's policy will also check for permissions (performantly!) on the relevant resources.
@user.policy(@squad).view?
# ^^ Does the user have view access to the squad, its team, or its organization?
@user.policy(@squad).modify?
# ^^ Does the user have modify access to the squad, its team, or its organization?
6. Applying permissions to a resource
Permissions are polymorphic to a resource; this means you can apply permissions to anything which has a Recurso::Resource
concern applied to it. Doing so is as you'd expect:
@user..create(resource: @team, level: :admin)
# ^^ make this user an admin of this team
@user..create(resource: @organization, level: :editor)
# ^^ give this user editor righrs for this organization
7. Permission policies
Recurso has the concept of a 'default' permission level (default
by default). This rights granted by this permission level can change based on the policy_type
of the model in question.
There are three policy types available out of the box (this can be configured with options described below):
- Users with
default
permission on a resource that isopen
canview
andedit
content - Users with
default
permission on a resource that isclosed
canview
content - Users with
default
permission on a resource that issecret
can do neither
Permission policies will cascade upwards if a level is not set. For example:
class Team
def relevant_association_names
[:itself, :organization]
end
end
@team = Team.create(organization: @organization, policy_type: nil)
@organization.update(policy_type: :closed)
@team.relevant_policy_type # => :closed
8. Configuration options
Recurso provides a set of granular configuration options to customize it to work the way you need.
An updated list, as well as all defaults can be viewed in lib/recurso/config.rb
levels_for_action:
A hash which maps which levels enable which actions. For instance, passing
{
view: [:viewer, :editor],
edit: [:editor]
}
will create a system where users with viewer
or editor
permission may view a resource, and users with editor
permission may edit a resource.
actions_for_default:
A hash which maps a permission_policy
to actions that the default level can perform. For instance, passing
{
open: [:view, :edit],
readonly: [:view]
}
will create a system where resources with a permission_policy
of open
allows default members to view
and edit
, while readonly
resources will only allow members to view
.
levels:
A list of valid levels which can be applied to your permissions
default_level:
The default value of the permissions.level
column, and the one which will be affected by a resource's permission_policy
(described above)
identity_foreign_key:
The foreign_key linking the permissions table to your identity table. By default, this is identity_id
, but could easily be user_id
or person_id
depending on the existing columns in your database.
permission_class_name:
The name of the class that holds the permissions (defaults to Permission
).
Both identity_foreign_key
and permission_class_name
accept lambdas. This is perfect if you want to support multiple models for authentication:
Recurso::Config.instance. = lambda do |model|
case model
when CustomIdentity then 'CustomPermission'
else 'Permission'
end
end
Recurso::Config.instance.identity_foreign_key = lambda do |model|
case model
when CustomIdentity then :identity_id
else :user_id
end
end
global_relations:
The names of relations you're interested in accessing globally. This expects an array of symbols, which will be constantized in order to find a class name
Recurso::Config.instance.global_relations = [:organizations, :teams, :squads]
This enables querying the Recurso::Global
for all models of that class.
e.g.:
# return all teams in the database which this user can view
user.(:teams)
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake test
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/recurso. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the Recurso project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.