Passing params to CanCan in RoR
In ApplicationController add the following:
# CanCan - pass params in to Ability
# https://github.com/ryanb/cancan/issues/133
def current_ability
@current_ability ||= Ability.new(current_user, params)
end
Accessing get params in CanCan
I recently achieved something similar using the method described here:
https://github.com/ryanb/cancan/wiki/Accessing-request-data
In my case, it looked something like this:
app/controllers/application_controller.rb:
class ApplicationController < ActionController::Base
...
def current_ability
@current_ability ||= Ability.new(current_user, params[:token])
end
end
app/models/ability.rb:
class Ability
include CanCan::Ability
def initialize(user, token=nil)
...
can :read, Article, :tokens => { :token => token }
...
end
end
Cancancan check submitted parameter
How to change what the Ability class can access:
https://github.com/ryanb/cancan/wiki/Accessing-Request-Data
Or how to pass params into the Ability class more specifically for your purposes:
https://stackoverflow.com/a/9472881/4880924
# CanCan - pass params in to Ability
# https://github.com/ryanb/cancan/issues/133
def current_ability
@current_ability ||= Ability.new(current_user, params)
end
Then it's a matter of simply accessing the relevant part of the params and checking whether it passes.
Checking action parameters in Rails CanCanCan Authorization
If I understand correctly, you are using cancan's authorize_resource
or load_and_authorize_resource
controller helper that calculates user abilities based on controller actions names.
But it's not obligatory to use this helper for all actions. You can skip it for actions having complex ability logic and check abilities manually.
For example:
class ParticipationsController < ApplicationController
authorize_resource except: :create # skiping `authorize_resource` for `create` action
# ...
def create
if creator_adds_someone_to_event?
authorize! :add_to, @event
end
if user_signs_up_for_event?
authorize! :sign_up_for, @event
end
# ...
end
So, you can check many different abilities in the same controller action. Just disable default cancancan's
behaviour for the action.
Rails Cancancan authorization of models in many-to-many relation
Here is an answer, a solution I have used in the end.
Background
Many-to-many relation is by definition complex and I do not think there are any simple solutions that fit all cases. Certainly, Ability in CanCanCan does not support it in default (unless you do some complicated hacks, such as the way the OP wanted to avoid, as mentioned in the Question).
In this particular case of question, however, the situation which the OP wants to deal with is a constraint based on the user ID, which is basically a one-to-many (or has_many
) relation, namely one-user having many roles. Then, it can actually fit in the standard way as Cancancan/Ability works.
General speaking, there are three ways to deal with the OP's case of many-to-many relation between users and roles (i.e., each user can have many roles and a role may belong to many users):
- handling it as in the User (Controller) model,
- handling it as in the Role (Controller) model,
- or UserRoleAssoc (Controller), that is, a model associated with the join table between User and Role (n.b., this Controller is not created by default and so you must create it manually if you use it).
Let me discuss below which one of the three best fits the purpose with Cancancan authorization.
How Cancancan authorizes with Ability and what would fit this case best
For the default CRUD actions, Cancancan deals with a can
statement as follows (in my understanding); this is basically a brief summary with regard to this case of the official reference of Cancancan:
- for the action
index
, only the information Cancancan has is the User, the Model Class (with/without scopes), in addition to the action typeindex
. So, basically, Cancancan does not and cannot do much. Importantly, a Ruby block associated with thecan
statement, if any, is not called. - if the (primary) ID of the model is given in the path, namely for the actions of
show
,edit
,update
,destroy
, Cancancan retrieves the model from the DB and it is fed to the algorithm you provide with thecan
statement, including a Ruby block, if given.
In the OP's case, a user should not be authorized to handle the roles of any other users but of her/himself. Then, the judgement must be based on the two user-IDs, i.e., the one of current_user
and the one given in the path/route. For Rails to pick up the latter from the path automatically, the route must be set accordingly.
Then, because the "ID" is a User-ID, the most natural solution to deal with this case is to use UsersController (case 1 in the description above); then the ID included in the default route is interpreted as User#id
by Rails and Cancancan. By contrast, if you adopt case 2, the default ID in the path will be interpreted as Role#id
, which does not work well with this case. As for case 3 (which was mentioned in the question), UserRoleAssoc#id
is just a random number given to an association and has nothing to do with User#id
or Role#id
. Therefore, it does not fit this case, either.
Solution
As explained above, the action of the Controller must be selected carefully so that Cancancan correctly sets the User based on the given ID in the path.
The OP mentions create
and delete
(destroy
) for the Controller. It is technically true in this case that the required actions are either or both of to create and delete new associations between a User and Roles. However, in Rails' default routing, create
does not take the ID parameter (of course not, given the ID is given in creation by the DB!). Therefore, the action name of create
is not really appropriate in this case. update
would be most appropriate. In the natural language, we interpret it such that a user's (Role-association) status will be update-d with this action of a Controller. The default HTTP method for update
is PATCH/PUT
, which fits the meaning of the operation, too.
Finally, here is the solution I have found to work (with Rails 6.1):
routes.rb
resources :manage_user_roles, only: [:update]
# => Route: manage_user_role PATCH /manage_user_roles/:id(.:format) manage_user_roles#update
manage_user_roles_controller.rb
class ManageUserRolesController < ApplicationController
load_and_authorize_resource :user
# This means as far as authorization is concerned,
# the model and controller are User and UsersController.
my_params = params.permit('add_role_11', 'del_role_11', 'add_role_12', 'del_role_12')
end
View (to submit the data)
This can be in show.html.erb
of User or whatever.
<%= form_with(method: :patch, url: manage_user_role_path(@user)) do |form| %>
Form components follow...
app/models/ability.rb
def initialize(user)
if user.present?
can :update, User, id: user.id
end
end
A key take is, I think, simplifying the case. Though many-to-many relations are inherently complex, you probably better deal with each case in smaller and more simple fragments. Then, they may fit in the existing scheme without too much hustle.
Related Topics
You May Have Encountered a Bug in the Ruby Interpreter or Extension Libraries
Ruby Copy a Paperclip Attachment from One Model to Another
HTML Is Read Before Fully Loaded Using Open-Uri and Nokogiri
Nokogiri Requires Ruby Version < 2.3
Finding Common String in Array of Strings (Ruby)
Why Does Ruby Only Permit Certain Operator Overloading
Get All Variables Defined Through 'Attr_Accessor' Without Overriding 'Attr_Accessor'
Regular Expression Matching Emoji in MAC Os X/Ios
Get Gem Vendor Files in Asset Pipeline Path
No Implicit Conversion from Nil to Integer - When Trying to Add Anything to Array
Unicode Characters in a Ruby Script
How to Sign_In for Devise to Test Controller with Minitest
Suppresing Output to Console with Ruby
I = True and False in Ruby Is True
What's the "Ruby Way" to Parse a String for a Single Key/Value