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/)