README.md in consul-1.3.1 vs README.md in consul-1.3.2

- old
+ new

@@ -1,22 +1,18 @@ -Consul — A next gen authorization solution -========================================== +# Consul — A next gen authorization solution [![Tests](https://github.com/makandra/consul/workflows/Tests/badge.svg)](https://github.com/makandra/consul/actions) [![Code Climate](https://codeclimate.com/github/makandra/consul.png)](https://codeclimate.com/github/makandra/consul) +Consul is an authorization solution for Ruby on Rails where you describe _sets of accessible things_ to control what a user can see or edit. -Consul is an authorization solution for Ruby on Rails where you describe *sets of accessible things* to control what a user can see or edit. - We have used Consul in combination with [assignable_values](https://github.com/makandra/assignable_values) to solve a variety of authorization requirements ranging from boring to bizarre. Also see our crash course video: [Solving bizare authorization requirements with Rails](http://bizarre-authorization.talks.makandra.com/). -Consul is tested with Rails 5.2, 6.1 and 7.0 on Ruby 2.5, 2.7 and 3.0 (only if supported, for each Ruby/Rails combination). If you need support for Rails 3.2, please use [v0.13.2](https://github.com/makandra/consul/tree/v0.13.2). +Consul is tested with Rails 5.2, 6.1, 7.0, 7.1 on Ruby 2.5, 2.7, 3.2, 3.3 (only if supported, for each Ruby/Rails combination). If you need support for Rails 3.2, please use [v0.13.2](https://github.com/makandra/consul/tree/v0.13.2). +## Describing access to your application -Describing access to your application -------------------------------------- - You describe access to your application by putting a `Power` model into `app/models/power.rb`. Inside your `Power` you can talk about what is accessible for the current user, e.g. - [A scope of records a user may see](#scope-powers-relations) - [Whether the user is allowed to use a particular screen](#boolean-powers) @@ -49,14 +45,12 @@ There are no restrictions on the name or constructor arguments of this class. You can deposit all kinds of objects in your power. See the sections below for details. - ### Scope powers (relations) - A typical use case in a Rails application is to restrict access to your ActiveRecord models. For example: - Anonymous visitors may only see public posts - Users may only see their own notes - Only admins may edit users @@ -109,17 +103,14 @@ power.note!(Note.last) # => raises Consul::Powerless unless the given Note is in the Power#notes scope ``` See our crash course video [Solving bizare authorization requirements with Rails](http://bizarre-authorization.talks.makandra.com/) for many different use cases you can cover with this pattern. - - ### Defining different powers for different actions If you have different access rights for e.g. viewing or updating posts, simply use different powers: - ```rb class Power ... power :notes do @@ -137,12 +128,10 @@ end ``` There is also a [shortcut to map different powers to RESTful controller actions](#protect-entry-into-controller-actions). - - ### Boolean powers Boolean powers are useful to control access to stuff that doesn't live in the database: ```rb @@ -162,11 +151,10 @@ power = Power.new(@user) power.dashboard? # => true power.dashboard! # => raises Consul::Powerless unless Power#dashboard? returns true ``` - ### Powers that give no access at all Note that there is a difference between having access to an empty list of records, and having no access at all. If you want to express that a user has no access at all, make the respective power return `nil`. @@ -192,20 +180,17 @@ power.users! # => raises Consul::Powerless power.user?(User.last) # => returns false power.user!(User.last) # => raises Consul::Powerless ``` - - ### Powers that only check a given object Sometimes it is not convenient to define powers as a collection or scope (relation). Sometimes you only want to store a method that checks whether a given object is accessible. To do so, simply define a power that ends in a question mark: - ```rb class Power ... power :updatable_post? do |post| @@ -221,11 +206,10 @@ power = Power.new(@user) power.updatable_post?(Post.last) # return true if the author of the post is @user power.updatable_post!(Post.last) # raises Consul::Powerless unless the author of the post is @user ``` - ### Other types of powers A power can return any type of object. For instance, you often want to return an array: ```rb @@ -252,11 +236,10 @@ power.assignable_note_state?('draft') # => returns true power.assignable_note_state?('published') # => returns false power.assignable_note_state!('published') # => raises Consul::Powerless ``` - ### Defining multiple powers at once You can define multiple powers at once by giving multiple power names: ```rb @@ -268,11 +251,10 @@ end end ``` - ### Powers that require context (arguments) Sometimes it can be useful to define powers that require context. To do so, just take an argument in your `power` block: ```rb @@ -292,11 +274,10 @@ client = ... note = ... Power.current.client_note?(client, note) ``` - ### Optimizing record checks for scope powers You can query a scope power for a given record, e.g. ```rb @@ -335,14 +316,12 @@ end ``` This way you do not need to touch the database at all. +## Role-based permissions -Role-based permissions ----------------------- - Consul has no built-in support for role-based permissions, but you can easily implement it yourself. Let's say your `User` model has a string column `role` which can be `"author"` or `"admin"`: ```rb class Power include Consul::Power @@ -365,14 +344,12 @@ end end ``` +## Controller integration -Controller integration ----------------------- - It is convenient to expose the power for the current request to the rest of the application. Consul will help you with that if you tell it how to instantiate a power for the current request: ```rb class ApplicationController < ActionController::Base include Consul::Controller @@ -396,22 +373,21 @@ end end ``` - ### Protect entry into controller actions To make sure a power is given before every action in a controller: ```rb class NotesController < ApplicationController power :notes end ``` -You can use `:except` and `:only` options like in before\_actions. +You can use `:except` and `:only` options like in before_actions. You can also map different powers to different actions: ```rb class NotesController < ApplicationController @@ -439,11 +415,10 @@ class NotesController < ApplicationController power :crud => :notes end ``` - And if your power [requires context](#powers-that-require-context-arguments) (is parametrized), you can give it using the `:context` method: ```rb class ClientNotesController < ApplicationController @@ -456,12 +431,10 @@ end end ``` - - ### Auto-mapping a power scope to a controller method It is often convenient to map a power scope to a private controller method: ```rb @@ -484,19 +457,18 @@ class NotesController < ApplicationController power :notes, :as => :note_scope # ... - + def note_scope super.where(trashed: false) end end ``` - ### Multiple power-mappings for nested resources When using [nested resources](http://guides.rubyonrails.org/routing.html#nested-resources) you probably want two power checks and method mappings: One for the parent resource, another for the child resource. @@ -562,24 +534,22 @@ include Consul::Controller require_power_check end ``` -Note that this check is satisfied by *any* `.power` directive in the controller class or its ancestors, even if that `.power` directive has `:only` or `:except` options that do not apply to the current action. +Note that this check is satisfied by _any_ `.power` directive in the controller class or its ancestors, even if that `.power` directive has `:only` or `:except` options that do not apply to the current action. Should you want to forego the power check (e.g. to remove authorization checks from an entirely public controller): ```rb class ApiController < ApplicationController skip_power_check end ``` +## Validating assignable values -Validating assignable values ----------------------------- - Sometimes a scope is not enough to express what a user can edit. You will often want to give a user write access to a record, but restrict the values she can assign to a given field. Consul leverages the [assignable_values](https://github.com/makandra/assignable_values) gem to add an optional authorization layer to your models. This layer adds additional validations in the context of a request, but skips those validations in other contexts (console, background jobs, etc.). You can enable the authorization layer by using the macro `authorize_values_for`: @@ -645,12 +615,11 @@ ```rb assignable_values_for :field, :through => lambda { Power.current } ``` -Memoization ------------ +## Memoization All power methods are [memoized](https://www.justinweiss.com/articles/4-simple-memoization-patterns-in-ruby-and-one-gem/) for performance reasons. Multiple calls to the same method will only call your block the first time, and return a cached result afterwards: ``` power = Power.new @@ -663,14 +632,12 @@ ``` power.unmemoize_all ``` +## Dynamic power access -Dynamic power access --------------------- - Consul gives you a way to dynamically access and query powers for a given name, model class or record. A common use case for this are generic helper methods, e.g. a method to display an "edit" link for any given record if the user is authorized to change that record: ```rb @@ -685,35 +652,32 @@ end ``` You can find a full list of available dynamic calls below: -| Dynamic call | Equivalent | -|---------------------------------------------------------|--------------------------------------------| -| `Power.current.send(:notes)` | `Power.current.notes` | -| `Power.current.include_power?(:notes)` | `Power.current.notes?` | -| `Power.current.include_power!(:notes)` | `Power.current.notes!` | -| `Power.current.include_object?(:notes, Note.last)` | `Power.current.note?(Note.last)` | -| `Power.current.include_object!(:notes, Note.last)` | `Power.current.note!(Note.last)` | -| `Power.current.for_model(Note)` | `Power.current.notes` | -| `Power.current.for_model(:updatable, Note)` | `Power.current.updatable_notes` | -| `Power.current.include_model?(Note)` | `Power.current.notes?` | -| `Power.current.include_model?(:updatable, Note)` | `Power.current.updatable_notes?` | -| `Power.current.include_model!(Note)` | `Power.current.notes!` | -| `Power.current.include_model!(:updatable, Note)` | `Power.current.updatable_notes!` | -| `Power.current.include_record?(Note.last)` | `Power.current.note?(Note.last)` | -| `Power.current.include_record?(:updatable, Note.last)` | `Power.current.updatable_note?(Note.last)` | -| `Power.current.include_record!(Note.last)` | `Power.current.note!(Note.last)` | -| `Power.current.include_record!(:updatable, Note.last)` | `Power.current.updatable_note!(Note.last)` | -| `Power.current.name_for_model(Note)` | `:notes` | -| `Power.current.name_for_model(:updatable, Note)` | `:updatable_notes` | +| Dynamic call | Equivalent | +| ------------------------------------------------------ | ------------------------------------------ | +| `Power.current.send(:notes)` | `Power.current.notes` | +| `Power.current.include_power?(:notes)` | `Power.current.notes?` | +| `Power.current.include_power!(:notes)` | `Power.current.notes!` | +| `Power.current.include_object?(:notes, Note.last)` | `Power.current.note?(Note.last)` | +| `Power.current.include_object!(:notes, Note.last)` | `Power.current.note!(Note.last)` | +| `Power.current.for_model(Note)` | `Power.current.notes` | +| `Power.current.for_model(:updatable, Note)` | `Power.current.updatable_notes` | +| `Power.current.include_model?(Note)` | `Power.current.notes?` | +| `Power.current.include_model?(:updatable, Note)` | `Power.current.updatable_notes?` | +| `Power.current.include_model!(Note)` | `Power.current.notes!` | +| `Power.current.include_model!(:updatable, Note)` | `Power.current.updatable_notes!` | +| `Power.current.include_record?(Note.last)` | `Power.current.note?(Note.last)` | +| `Power.current.include_record?(:updatable, Note.last)` | `Power.current.updatable_note?(Note.last)` | +| `Power.current.include_record!(Note.last)` | `Power.current.note!(Note.last)` | +| `Power.current.include_record!(:updatable, Note.last)` | `Power.current.updatable_note!(Note.last)` | +| `Power.current.name_for_model(Note)` | `:notes` | +| `Power.current.name_for_model(:updatable, Note)` | `:updatable_notes` | +## Querying a power that might be nil - -Querying a power that might be nil ----------------------------------- - You will often want to access `Power.current` from another model, to e.g. iterate through the list of accessible users: ```rb class UserReport @@ -758,28 +722,25 @@ end ``` There is a long selection of class methods that behave neutrally in case `Power.current` is `nil`: -| Call | Equivalent | -|----------------------------------------------------------|---------------------------------------------------------------------| -| `Power.for_model(Note)` | `Power.current.present? ? Power.current.notes : Note` | -| `Power.for_model(:updatable, Note)` | `Power.current.present? ? Power.current.updatable_notes : Note` | -| `Power.include_model?(Note)` | `Power.current.present? ? Power.notes? : true` | -| `Power.include_model?(:updatable, Note)` | `Power.current.present? ? Power.updatable_notes? : true` | -| `Power.include_model!(Note)` | `Power.notes! if Power.current.present?` | -| `Power.include_model!(:updatable, Note)` | `Power.updatable_notes! if Power.current.present?` | -| `Power.include_record?(Note.last)` | `Power.current.present? ? Power.note?(Note.last) : true` | -| `Power.include_record?(:updatable, Note.last)` | `Power.current.present? ? Power.updatable_note?(Note.last?) : true` | -| `Power.include_record!(Note.last)` | `Power.note!(Note.last) if Power.current.present?` | -| `Power.include_record!(:updatable, Note.last)` | `Power.updatable_note!(Note.last) if Power.current.present?` | +| Call | Equivalent | +| ---------------------------------------------- | ------------------------------------------------------------------- | +| `Power.for_model(Note)` | `Power.current.present? ? Power.current.notes : Note` | +| `Power.for_model(:updatable, Note)` | `Power.current.present? ? Power.current.updatable_notes : Note` | +| `Power.include_model?(Note)` | `Power.current.present? ? Power.notes? : true` | +| `Power.include_model?(:updatable, Note)` | `Power.current.present? ? Power.updatable_notes? : true` | +| `Power.include_model!(Note)` | `Power.notes! if Power.current.present?` | +| `Power.include_model!(:updatable, Note)` | `Power.updatable_notes! if Power.current.present?` | +| `Power.include_record?(Note.last)` | `Power.current.present? ? Power.note?(Note.last) : true` | +| `Power.include_record?(:updatable, Note.last)` | `Power.current.present? ? Power.updatable_note?(Note.last?) : true` | +| `Power.include_record!(Note.last)` | `Power.note!(Note.last) if Power.current.present?` | +| `Power.include_record!(:updatable, Note.last)` | `Power.updatable_note!(Note.last) if Power.current.present?` | +## Testing - -Testing -------- - This section Some hints for testing authorization with Consul. ### Test that a controller checks against a power Include the Consul Matcher `spec/support/consul_matchers.rb`: @@ -845,26 +806,22 @@ Power.without_power do # run code that should not see a Power end ``` +## Installation -Installation ------------- - Add the following to your `Gemfile`: ``` gem 'consul' ``` Now run `bundle install` to lock the gem into your project. +## Development -Development ------------ - We currently develop using Ruby 2.5.3 (see `.ruby-version`) since that version works for current versions of ActiveRecord that we support. GitHub Actions will test additional Ruby versions (2.3.8, 2.4.5, and 3.0.1). There are tests in `spec`. We only accept PRs with tests. To run tests: - Install Ruby 2.5.3 @@ -882,10 +839,8 @@ Note that we have configured GitHub Actions to automatically run tests in all supported Ruby versions and dependency sets after each push. We will only merge pull requests after a green GitHub Actions run. I'm very eager to keep this gem leightweight and on topic. If you're unsure whether a change would make it into the gem, [talk to me beforehand](mailto:henning.koch@makandra.de). - -Credits -------- +## Credits Henning Koch from [makandra](http://makandra.com/)