Skip to content

Commit

Permalink
Add: wiki documentation into a separate docs folder (#621)
Browse files Browse the repository at this point in the history
  • Loading branch information
benkoshy authored May 14, 2020
1 parent 7bf836c commit d56d79d
Show file tree
Hide file tree
Showing 41 changed files with 2,575 additions and 0 deletions.
124 changes: 124 additions & 0 deletions docs/Abilities-in-Database.md
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
```
47 changes: 47 additions & 0 deletions docs/Ability-Precedence.md
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]]
34 changes: 34 additions & 0 deletions docs/Ability-for-Other-Users.md
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.
25 changes: 25 additions & 0 deletions docs/Accessing-request-data.md
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.
32 changes: 32 additions & 0 deletions docs/Action-Aliases.md
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.
51 changes: 51 additions & 0 deletions docs/Authorization-for-Namespaced-Controllers.md
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).
Loading

0 comments on commit d56d79d

Please sign in to comment.