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).resources_with_permission(:teams)
# ^^ which teams can I view within this organization?

@user.policy(@team).resources_with_permission(: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.resources_with_permission(: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
  authorize @team, :view?
end

def update
  authorize @team, :modify?
end

def destroy
  authorize @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 Squads 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.permissions.create(resource: @team, level: :admin)
# ^^ make this user an admin of this team

@user.permissions.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 is open can view and edit content
  • Users with default permission on a resource that is closed can view content
  • Users with default permission on a resource that is secret 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.permission_class_name = 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.resources_with_permission(: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.