-
-
Notifications
You must be signed in to change notification settings - Fork 640
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add: wiki documentation into a separate docs folder (#621)
- Loading branch information
Showing
41 changed files
with
2,575 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
What if you or a client, wants to change permissions without having to re-deploy the application? | ||
In that case, it may be best to store the permission logic in a database: it is very easy to use the database records when defining abilities. | ||
|
||
We will need a model called `Permission`. | ||
|
||
Each user `has_many :permissions`, and each permission has `action`, `subject_class` and `subject_id` columns. The last of which is optional. | ||
|
||
```ruby | ||
class Ability | ||
include CanCan::Ability | ||
|
||
def initialize(user) | ||
can do |action, subject_class, subject| | ||
user.permissions.where(action: aliases_for_action(action)).any? do |permission| | ||
permission.subject_class == subject_class.to_s && | ||
(subject.nil? || permission.subject_id.nil? || permission.subject_id == subject.id) | ||
end | ||
end | ||
end | ||
end | ||
``` | ||
|
||
An alternative approach is to define a separate `can` ability for each permission. | ||
|
||
```ruby | ||
def initialize(user) | ||
user.permissions.each do |permission| | ||
if permission.subject_id.nil? | ||
can permission.action.to_sym, permission.subject_class.constantize | ||
else | ||
can permission.action.to_sym, permission.subject_class.constantize, id: permission.subject_id | ||
end | ||
end | ||
end | ||
``` | ||
|
||
The actual details will depend largely on your application requirements, but hopefully, you can see how it's possible to define permissions in the database and use them with CanCanCan. | ||
|
||
You can mix-and-match this with defining permissions in the code as well. This way you can keep the more complex logic in the code so you don't need to shoe-horn every kind of permission rule into an overly-abstract database. | ||
|
||
|
||
You can also create a `Permission` model containing all possible permissions in your app. Use that code to create a rake task that fills a `Permission` table: | ||
(The code below is not fully tested) | ||
|
||
To use the following code, the permissions table should have such fields :name, :user_id, :subject_class, :subject_id, :action, and :description.You can generate the permission model by the command: `rails g model Permission user_id:integer name:string subject_class:string subject_id:integer action:string description:text`. | ||
|
||
```ruby | ||
class ApplicationController < ActionController::Base | ||
... | ||
protected | ||
|
||
# Derive the model name from the controller. UsersController will return User | ||
def self.permission | ||
return name = controller_name.classify.constantize | ||
end | ||
end | ||
``` | ||
|
||
```ruby | ||
def setup_actions_controllers_db | ||
|
||
write_permission("all", "manage", "Everything", "All operations", true) | ||
|
||
controllers = Dir.new("#{Rails.root}/app/controllers").entries | ||
controllers.each do |controller| | ||
if controller =~ /_controller/ | ||
foo_bar = controller.camelize.gsub(".rb","").constantize.new | ||
end | ||
end | ||
# You can change ApplicationController for a super-class used by your restricted controllers | ||
ApplicationController.subclasses.each do |controller| | ||
if controller.respond_to?(:permission) | ||
klass, description = controller.permission | ||
write_permission(klass, "manage", description, "All operations") | ||
controller.action_methods.each do |action| | ||
if action.to_s.index("_callback").nil? | ||
action_desc, cancan_action = eval_cancan_action(action) | ||
write_permission(klass, cancan_action, description, action_desc) | ||
end | ||
end | ||
end | ||
end | ||
|
||
end | ||
|
||
|
||
def eval_cancan_action(action) | ||
case action.to_s | ||
when "index", "show", "search" | ||
cancan_action = "read" | ||
action_desc = I18n.t :read | ||
when "create", "new" | ||
cancan_action = "create" | ||
action_desc = I18n.t :create | ||
when "edit", "update" | ||
cancan_action = "update" | ||
action_desc = I18n.t :edit | ||
when "delete", "destroy" | ||
cancan_action = "delete" | ||
action_desc = I18n.t :delete | ||
else | ||
cancan_action = action.to_s | ||
action_desc = "Other: " << cancan_action | ||
end | ||
return action_desc, cancan_action | ||
end | ||
|
||
def write_permission(class_name, cancan_action, name, description, force_id_1 = false) | ||
permission = Permission.find(:first, :conditions => ["subject_class = ? and action = ?", class_name, cancan_action]) | ||
if not permission | ||
permission = Permission.new | ||
permission.id = 1 if force_id_1 | ||
permission.subject_class = class_name | ||
permission.action = cancan_action | ||
permission.name = name | ||
permission.description = description | ||
permission.save | ||
else | ||
permission.name = name | ||
permission.description = description | ||
permission.save | ||
end | ||
end | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
An ability rule will override a previous one. | ||
For example, let's say we want the user to be able to do everything to projects except destroy them. | ||
|
||
This is the correct way: | ||
|
||
```ruby | ||
can :manage, Project | ||
cannot :destroy, Project | ||
``` | ||
|
||
It is important that the `cannot :destroy` line comes after the `can :manage` line. If they were reversed, `cannot :destroy` would be overridden by `can :manage`. | ||
|
||
Adding `can` rules do not override prior rules, but instead are logically or'ed. | ||
|
||
```ruby | ||
can :manage, Project, user_id: user.id | ||
can :update, Project do |project| | ||
!project.locked? | ||
end | ||
``` | ||
|
||
For the above, `can? :update` will always return true if the `user_id` equals `user.id`, even if the project is locked. | ||
|
||
This is also important when dealing with roles which have inherited behavior. For example, let's say we have two roles, moderator and admin. We want the admin to inherit the moderator's behavior. | ||
|
||
```ruby | ||
if user.role? :moderator | ||
can :manage, Project | ||
cannot :destroy, Project | ||
can :manage, Comment | ||
end | ||
|
||
if user.role? :admin | ||
can :destroy, Project | ||
end | ||
``` | ||
|
||
Here it is important the admin role be after the moderator so it can override the `cannot` behavior to give the admin more permissions. See [[Role Based Authorization]]. | ||
|
||
If you are not getting the behavior you expect, please [[post an issue|https://github.com/CanCanCommunity/cancancan/issues]]. | ||
|
||
## Additional Docs | ||
|
||
* [[Defining Abilities]] | ||
* [[Checking Abilities]] | ||
* [[Debugging Abilities]] | ||
* [[Testing Abilities]] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
What if you want to determine the abilities of a `User` record that is not the `current_user`? Maybe we want to see if another user can update an article. | ||
|
||
```ruby | ||
some_user.ability.can? :update, @article | ||
``` | ||
|
||
You can easily add an `ability` method in the `User` model. | ||
|
||
```ruby | ||
def ability | ||
@ability ||= Ability.new(self) | ||
end | ||
``` | ||
|
||
I also recommend adding delegation so `can?` can be called directly from the user. | ||
|
||
```ruby | ||
class User < ActiveRecord::Base | ||
delegate :can?, :cannot?, to: :ability | ||
# ... | ||
end | ||
|
||
some_user.can? :update, @article | ||
``` | ||
|
||
Finally, if you're using this approach, it's best to override the `current_ability` method in the `ApplicationController` so it uses the same method. | ||
|
||
```ruby | ||
def current_ability | ||
@current_ability ||= current_user.ability | ||
end | ||
``` | ||
|
||
The downside of this approach is that [[Accessing Request Data]] is not as easy, so it depends on the needs of your application. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
What if you need to modify the permissions based on something outside of the User object? For example, let's say you want to blacklist certain IP addresses from creating comments. The IP address is accessible through request.remote_ip but the Ability class does not have access to this. It's easy to modify what you pass to the Ability object by overriding the current_ability method in ApplicationController. | ||
|
||
```ruby | ||
class ApplicationController < ActionController::Base | ||
#... | ||
|
||
private | ||
|
||
def current_ability | ||
@current_ability ||= Ability.new(current_user, request.remote_ip) | ||
end | ||
end | ||
``` | ||
```ruby | ||
class Ability | ||
include CanCan::Ability | ||
|
||
def initialize(user, ip_address=nil) | ||
can :create, Comment unless BLACKLIST_IPS.include? ip_address | ||
end | ||
end | ||
``` | ||
This concept can apply to session and cookies as well. | ||
|
||
You may wonder, why I pass only the IP Address instead of the entire request object? I prefer to pass only the information needed, this makes testing and debugging the behavior easier. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
You will usually be working with four actions when [[defining|Defining Abilities]] and [[checking|Checking Abilities]] permissions: `:read`, `:create`, `:update`, `:destroy`. These aren't the same as the 7 RESTful actions in Rails. CanCanCan automatically adds some convenient aliases for mapping the controller actions. | ||
|
||
```ruby | ||
alias_action :index, :show, :to => :read | ||
alias_action :new, :to => :create | ||
alias_action :edit, :to => :update | ||
``` | ||
|
||
Notice the `edit` action is aliased to `update`. This means if the user is able to update a record he also has permission to edit it. You can define your own aliases in the `Ability` class. | ||
|
||
```ruby | ||
class Ability | ||
include CanCan::Ability | ||
def initialize(user) | ||
alias_action :update, :destroy, :to => :modify | ||
can :modify, Comment | ||
end | ||
end | ||
|
||
# in controller or view | ||
can? :update, Comment # => true | ||
``` | ||
|
||
You are not restricted to just the 7 RESTful actions, you can use any action name. See [[Custom Actions]] for details. | ||
|
||
Please note that if you are changing the default alias_actions, the original actions associated with the alias will NOT be removed. For example, following statement will not have any effect on the alias :read, which points to :show and :index: | ||
|
||
```ruby | ||
alias_action :show, :to => :read # this will have no effect on the alias :read! | ||
``` | ||
|
||
If you want to change the default actions, you should use clear_aliased_actions method to remove ALL default aliases first. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
The default operation for CanCanCan is to authorize based on user and the object identified in `load_resource`. So if you have a `WidgetsController` and also an `Admin::WidgetsController`, you can use some different approaches. | ||
|
||
Just like in the example given for [[Accessing Request Data]], you **can** also create differing authorization rules that depend on the controller namespace. | ||
|
||
In this case, just override the `current_ability` method in `ApplicationController` to include the controller namespace, and create an `Ability` class that knows what to do with it. | ||
|
||
``` ruby | ||
class Admin::WidgetsController < ActionController::Base | ||
#... | ||
|
||
private | ||
|
||
def current_ability | ||
# I am sure there is a slicker way to capture the controller namespace | ||
controller_name_segments = params[:controller].split('/') | ||
controller_name_segments.pop | ||
controller_namespace = controller_name_segments.join('/').camelize | ||
@current_ability ||= Ability.new(current_user, controller_namespace) | ||
end | ||
end | ||
|
||
|
||
class Ability | ||
include CanCan::Ability | ||
|
||
def initialize(user, controller_namespace) | ||
case controller_namespace | ||
when 'Admin' | ||
can :manage, :all if user.has_role? 'admin' | ||
else | ||
# rules for non-admin controllers here | ||
end | ||
end | ||
end | ||
``` | ||
|
||
Another way to achieve the same is to use a completely different Ability class in this controller: | ||
|
||
``` ruby | ||
class Admin::WidgetsController < ActionController::Base | ||
#... | ||
|
||
private | ||
|
||
def current_ability | ||
@current_ability ||= AdminAbility.new(current_user) | ||
end | ||
end | ||
``` | ||
|
||
and follow the [Best Practice of splitting your Ability file into multiple files](https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities%3A-Best-Practices#split-your-abilityrb-file). |
Oops, something went wrong.