针对 Ruby on Rails 的范围授权。
We have used Consul in combination with 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.
Consul is tested with Rails 6.1, 7.1, 7.2 and 8.0 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.
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 Power might look like this:
class Power
include Consul::Power
def initialize(user)
@user = 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
endThere 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.
A typical use case in a Rails application is to restrict access to your ActiveRecord models. For example:
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
endYou can now query these powers in order to retrieve the scope:
power = Power.new(user)
power.notes # => returns an ActiveRecord::ScopeOr 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 nilOr you can raise an error unless a power is given, e.g. to guard access into a controller action:
power.notes! # => raises Consul::Powerless unless 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):
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 scopeSee our crash course video Solving bizare authorization requirements with Rails for many different use cases you can cover with this pattern.
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
endThere is also a shortcut to map different powers to RESTful controller actions.
Boolean powers are useful to control access to stuff that doesn't live in the database:
class Power
...
power :dashboard do
true
end
endYou can query it like the other powers:
power = Power.new(@user)
power.dashboard? # => true
power.dashboard! # => raises Consul::Powerless unless Power#dashboard? returns trueNote 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:
class Power
...
power :users do
User if @user.admin?
end
endWhen a non-admin queries the :users power, she will get the following behavior:
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::PowerlessSometimes 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:
class Power
...
power :updatable_post? do |post|
post.author == @user
end
endYou 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 @userA power can return any type of object. For instance, you often want to return an array:
class Power
...
power :assignable_note_states do
if admin?
%w[draft pending published retracted]
else
%w[draft pending]
end
end
endYou can query it like any other power. E.g. if a non-admin queries this power she will get the following behavior:
power.assignable_note_states # => ['draft', 'pending']
power.assignable_note_states? # => returns true
power.assignable_note_states! # => does nothing (because the power isn't nil)
power.assignable_note_state?('draft') # => returns true
power.assignable_note_state?('published') # => returns false
power.assignable_note_state!('published') # => raises Consul::PowerlessYou can define multiple powers at once by giving multiple power names:
class Power
...
power :destroyable_users, :updatable_users do
User if admin?
end
endSometimes it can be useful to define powers that require context. To do so, just take an argument in your power block:
class Power
...
power :client_notes do |client|
client.notes.where(:state => 'published')
end
endWhen querying such a power, you always need to provide the context, e.g.:
client = ...
note = ...
Power.current.client_note?(client, note)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
endThis way you do not need to touch the database at all.
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":
class Power
include Consul::Power
def initialize(user)
@user = user
end
power :notes do
case role
when :admin then Note
when :author then Note.by_author
end
end
private
def role
@user.role.to_sym
end
endIt 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:
class ApplicationController < ActionController::Base
include Consul::Controller
current_power do
Power.new(current_user)
end
endYou now have a helper method current_power for your controller and views. Everywhere else, you can access it from Power.current. The power will be instantiated when the request is handed over from routing to ApplicationController, and will be nilified once the request was processed.
You can now use power scopes to control access:
class NotesController < ApplicationController
def show
@note = current_power.notes.find(params[:id])
end
endTo make sure a power is given before every action in a controller:
class NotesController < ApplicationController
power :notes
endYou can use :except and :only options like in before_actions.
You can also map different powers to different actions:
class NotesController < ApplicationController
power :notes, :map => { [:edit, :update, :destroy] => :changeable_notes }
endActions that are not listed in :map will get the default action :notes.
Note that in moderately complex authorization scenarios you will often find yourself writing a map like this:
class NotesController < ApplicationController
power :notes, :map => {
[:edit, :update] => :updatable_notes,
[:new, :create] => :creatable_notes,
[:destroy] => :destroyable_notes
}
endBecause this pattern is so common, there is a shortcut :crud to do the same:
class NotesController < ApplicationController
power :crud => :notes
endAnd if your power requires context (is parametrized), you can give it using the :context method:
class ClientNotesController < ApplicationController
power :client_notes, :context => :load_client
private
def load_client
@client ||= Client.find(params[:client_id])
end
endIt is often convenient to map a power scope to a private controller method:
class NotesController < ApplicationController
power :notes, :as => :note_scope
def show
@note = note_scope.find(params[:id])
end
endThe mapped method is aware of the :map option.
The mapped method can be overridden and access the original implementation using super:
class NotesController < ApplicationController
power :notes, :as => :note_scope
# ...
def note_scope
super.where(trashed: false)
end
endWhen using nested resources you probably want two power checks and method mappings: One for the parent resource, another for the ch
暂无开放 Issues,或尚未同步最近议题。