README.md in consul-0.8.0 vs README.md in consul-0.9.0
- old
+ new
@@ -1,70 +1,131 @@
-Consul - A scope-based authorization solution
-=============================================
+Consul - A next gen authorization solution
+==========================================
[](https://travis-ci.org/makandra/consul)
-Consul is a authorization solution for Ruby on Rails that uses scopes to control what a user can see or edit.
+Consul is a 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/).
-Describing a power for your application
----------------------------------------
+Describing access to your application
+-------------------------------------
-You describe access to your application by putting a `Power` model into `app/models/power.rb`:
+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)
+- [A list of values a user may assign to a particular attribute](#validating-assignable-values)
+
+A `Power` might look like this:
+
class Power
include Consul::Power
def initialize(user)
@user = user
end
- power :notes do
- Note.by_author(@user)
- end
-
power :users do
User if @user.admin?
end
+ power :notes do
+ Note.by_author(@user)
+ end
+
power :dashboard do
true # not a scope, but a boolean power. This is useful to control access to stuff that doesn't live in the database.
end
end
-There are no restrictions on the name or constructor arguments of your power class.
+There are no restrictions on the name or constructor arguments of your this class.
+You can deposit all kinds of objects in your power. See the sections below for details.
-Querying a power
-----------------
-Common things you might want from a power:
+### Scope powers (relations)
-1. Get its scope
-2. Ask whether it is there
-3. Raise an error unless it its there
-4. Ask whether a given record is included in its scope
-5. Raise an error unless a given record is included in its scope
-Here is how to do all of that:
+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
+
+You do this by making your powers return an ActiveRecord scope (or "relation"):
+
+ class Power
+ ...
+
+ power :notes do
+ Note.by_author(@user)
+ end
+
+ power :users do
+ User if @user.admin?
+ end
+
+ end
+
+You can now query these powers in order to retrieve the scope:
+
power = Power.new(user)
- power.notes # => returns an ActiveRecord::Scope
- power.notes? # => returns true if Power#notes returns a scope
- power.notes! # => raises Consul::Powerless unless Power#notes returns a scope
+ power.notes # => returns an ActiveRecord::Scope
+
+Or you can ask if the power is given (meaning it's not `nil`):
+
+ power.notes? # => returns true if Power#notes returns a scope and not nil
+
+Or you can raise an error unless a power its given, e.g. to guard access into a controller action:
+
+ power.notes? # => returns true if Power#notes returns a scope, even if it's empty
+
+Or you ask whether a given record is included in its scope (can be [optimized](#optimizing-record-checks-for-scope-powers)):
+
power.note?(Note.last) # => returns whether the given Note is in the Power#notes scope. Caches the result for subsequent queries.
+
+Or you can raise an error unless a given record is included in its scope:
+
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.
-Boolean powers
---------------
+
+### Defining different powers for different actions
+
+If you have different access rights for e.g. viewing or updating posts, simply use different powers:
+
+
+ class Power
+ ...
+
+ power :notes do
+ Note.published
+ end
+
+ power :updatable_notes do
+ Note.by_author(@user)
+ end
+
+ power :destroyable_notes do
+ Note if @user.admin?
+ end
+
+ 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:
class Power
...
@@ -74,16 +135,16 @@
end
You can query it like the other powers:
+ power = Power.new(@user)
power.dashboard? # => true
power.dashboard! # => raises Consul::Powerless unless Power#dashboard? returns true
-Powers that give no access at all
----------------------------------
+### 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`.
Note how the power in the example below returns `nil` unless the user is an admin:
@@ -97,20 +158,45 @@
end
When a non-admin queries the `:users` power, she will get the following behavior:
- power.notes # => returns nil
- power.notes? # => returns false
- power.notes! # => raises Consul::Powerless
- power.note?(Note.last) # => returns false
- power.note!(Note.last) # => raises Consul::Powerless
+ power = Power.new(@user)
+ power.users # => returns nil
+ power.users? # => returns false
+ power.users! # => raises Consul::Powerless
+ power.user?(User.last) # => returns false
+ power.user!(User.last) # => raises Consul::Powerless
-Other types of powers
----------------------
+### Powers that only check a given object
+
+Sometimes it is not convenient to define powers as a collection. 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:
+
+
+ class Power
+ ...
+
+ power :updatable_post? do |post|
+ post.author == @user
+ end
+
+ end
+
+You can query such an power as always:
+
+ 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:
class Power
...
@@ -132,27 +218,25 @@
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
---------------------------------
+### Defining multiple powers at once
You can define multiple powers at once by giving multiple power names:
class Power
...
- power :destroyable_users, updatable_users do
+ power :destroyable_users, :updatable_users do
User if admin?
end
end
-Powers that require context (arguments)
----------------------------------------
+### 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:
class Power
...
@@ -162,17 +246,58 @@
%w[delivered archived]
else
%w[committed started finished]
end
end
+
+ end
When querying such a power, you always need to provide the context, e.g.:
story = ...
- Power.current_assignable_story_state?(story, 'finished')
+ Power.current.assignable_story_state?(story, 'finished')
+### Optimizing record checks for scope powers
+
+You can query a scope power for a given record, e.g.
+
+ class Power
+ ...
+
+ power :posts do |post|
+ Post.where(:author_id => @user.id)
+ end
+ end
+
+ power = Power.new(@user)
+ power.post?(Post.last)
+
+What Consul does internally is fetch **all** the IDs of the `power.posts` scope and test if the given
+record's ID is among them. This list of IDs is cached for subsequent calls, so you will only touch the database once.
+
+As scary as it might sound, fetching all IDs of a scope scales quiet nicely for many thousand records. There will
+however be the point where you want to optimize this.
+
+What you can do in Consul is to define a second power that checks a given record in plain Ruby:
+
+ class Power
+ ...
+
+ power :posts do |post|
+ Post.where(:author_id => @user.id)
+ end
+
+ power :post? do |post|
+ post.author_id == @user.id
+ end
+
+ end
+
+This way you do not need to touch the database at all.
+
+
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"`:
@@ -377,13 +502,13 @@
You can find a full list of available dynamic calls below:
| Dynamic call | Equivalent |
|---------------------------------------------------------|--------------------------------------------|
| `Power.current.send(:notes)` | `Power.current.notes` |
-| `Power.current.include?(:notes)` | `Power.current.notes?` |
-| `Power.current.include!(:notes)` | `Power.current.notes!` |
-| `Power.current.include?(:notes, Note.last)` | `Power.current.note?(Note.last)` |
-| `Power.current.include!(:notes, Note.last)` | `Power.current.note!(Note.last)` |
+| `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_record(Note.last)` | `Power.current.notes` |
| `Power.current.for_record(:updatable, Note.last)` | `Power.current.updatable_notes` |
| `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?` |