# 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: ```ruby 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): ```ruby # 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: ```ruby 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: ```ruby @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: ```ruby @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)_ ```ruby @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! ```ruby # 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: ```ruby 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. ```ruby 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? %> <% end %> ``` if no resource is passed, the `default_policy_resource` will be used ```ruby def default_policy_resource @squad end ``` ```ruby 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 ```ruby 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. ```ruby @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: ```ruby @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: ```ruby class Team def relevant_association_names [:itself, :organization] end end ``` ```ruby @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 ```ruby { 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 ```ruby { 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: ```ruby 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 ```ruby Recurso::Config.instance.global_relations = [:organizations, :teams, :squads] ``` This enables querying the `Recurso::Global` for all models of that class. e.g.: ```ruby # 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](https://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](http://contributor-covenant.org) code of conduct. ## License The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). ## 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](https://github.com/gdpelican/recurso/blob/master/CODE_OF_CONDUCT.md).