Rails permit some_params differs depending on user - ruby-on-rails

In a Rails project I have a model with a "status" column/property
And different users can set different status values.
For example
not logged in you can create new record, and the status should always be set to 1
when logged in as user_type1 you can set the status to 2
when logged in as user_type2 you are allowed to update other attributes of the record, but not the status.
Currently I handle this by having multiple update methods, and different record_params permitting different update attribute lists. But I wonder if this is the right way. Having multiple update methods in the controller is a bit strange. But also permitting all the params all the time, and checking them within the method, I don't like either. How do you handle such situation

I think you can change the permitted params based on the type of the user somthing like the below,
def update_params
params.require(:record).permit(whitelisted_params)
end
def whitelisted_params
if %w[site_visitor user_type1].include?(user.user_type)
[:status]
elsif user.user_type == 'user_type2'
[:permitted_column_1, :permitted_column_2, :permitted_column_3]
end
end
And you said as the site visitor can always set the status as 1 means,
1 --> Merge the status value in the permitted params.
record_params.merge(status: 1)
2 --> Validating the status in the before_action
Calling this validator in the before_action and respond with error response.
before_action: validate_record_update
def validate_record_update
if %w[site_visitor user_type1].include?(user.user_type)
valid_status = params[:user][:status] == 1
elsif user.user_type == 'user_type2'
valid_status = params[:user][:status] == 2
end
unless valid_status
# Error response
end
end
3 --> Model level validator

If your application is getting large enough that you're dealing with this problem more often, it may be time to implement a Policy pattern to get more maintainable control of authorization in general, not just strong params.
Take a look through the documentation for the pundit gem, which makes policy implementation quite easy and allows you to maintain logic for different parameter permissioning: https://github.com/varvet/pundit#strong-parameters
The link has some great examples for implementing a solution to exactly this situation!

Related

Checking action parameters in Rails CanCanCan Authorization

Is it possible to access controller parameters when defining abilities in ability.rb?
I have an event and users that can participate in or create that event. It seems like I could create a different controller action for every possible scenario, e.g. a user signs himself up for an event or a creator deletes someone from the event. However I think it would be a lot easier to read to have less actions and be able to define abilities based on what parameters are being passed in from the client.
Answer
#chumakoff has some good info down below that helped explain how CanCanCan is working. I decided to authorize these actions by default in ability.rb, and then raise an error, e.g. raise CanCan::AccessDenied.new("You cannot delete someone else from this event"), in the controller if I detect incorrect user/event parameter IDs being sent in.
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.
Yes there is a debugging tool Named as " pry" . Use that it would help u out. Just use binding.pry wherever u want to check the value of parameters in the code and the console will stop executing at that moment so u can check the value of the parameters.

How can I check the authorization in Cancancan in Ruby on Rails each time the user calls that action?

I'm using Ruby on Rails 5 and I want that depending of the role of a User, the user can post 5 Posts or 10 or 15, and that is just a part of the several possible situations that I have to check for Authorization (for things that are not Posts for instance I have other situations that are a bit more complex), so I started using Cancancan (v 1.15.0) few days ago to don't make the User model too huge, so this way I have one class for each Role and in ability.rb I just merge the classes depending on the Role.
The problem is that using Cancancan apparently it checkes the Authorization only once. For example in Creating a Post, In Post#create the first line of code is:
authorize! :create, Post
In the Role class of the user, I have this code:
if user.posts.size < 10
Rails.logger.debug "If-size: #{user.posts.size}"
can :create, Post
else
Rails.logger.debug "Else-size: #{user.posts.size}"
cannot :create, Post
end
I have some tests with RSpec and I see the first time the current_user (the one with that specific role) creates a Post using the controller (I mean, not by FactoryGirl or any other way that is not using the controller), it appears in log/test.log:
If-size: 0
But neither the Else nor the If appears ever again in the log, it doesn't matter how many Posts I create by the Controller, it only evaluates this condition the first time and the User get authorized to creates as many Posts as he wants because the first time the condition in the If is true and it is not evaluated each time the method create of the controller Post is called.
EDIT: Solved by the method suggested by MarsAtomic. Thanks! :)
CanCanCan isn't meant to dynamically evaluate roles based on conditions within your application. That type of functionality is best handled by your application's own logic, rather than an authorization tool.
You should consider adding a column to your roles table that indicates how many posts a particular role is allowed. This column would allow you to check on the number of posts each user (via role) is allowed to create.
Then, in your posts_controller.rb, you can wrap your post creation logic in a block that runs only if the user has not exceeded the maximum number of posts allowed:
def create
if user.posts.size < user.role.max_posts
#post = Post.new(post_params)
#post.save
redirect_to #post
else
# flash an error on the page or redirect to an error page
end
end

Rails - add scope for certain controllers in a module based up a database setting

I have an api. In that api is a basecontroller that all other controllers inherit from. The basecontroller handles authentication and whether or not the API is on at all, etc.
There are users, and users can belong to a group. Users table has a group_id column.
I'm trying to introduce a new feature, whereby a select on the settings page for admin controls which users are shown from what groups. If an option is selected, only users from that group should be shown by the api.
I could go into each controller (there is one controller for each of a few different tasks - getting all users info, just active_users ids, a single users information, etc) and add the extra statement to each
if !settings.api_group.nil?
#add the additional where("group_id = ?, settings.group_id)
but that seems like a lot of repeating myself (doing it in 8 different places)
Is there some way to add something to the basecontroller that says:
if this setting option is not nil, only return user information if they are in this group
?
Thanks
You can add a method to the BaseController, and call it in each action that should have this restriction. Something like this:
in base_controller.rb:
protected
def filtered_users
if settings.api_group
User.where(:group_id => settings.group_id)
else
User.scoped
end
end
and in the controllers that inherit from it:
def index
#users = filtered_users
end
This way, you only define the filtering in one place. If it needs to change later, you only have to change it in one place. Because filtered_users actually returns a Relation, you can continue to alter the query by tacking additional .where clauses, etc, like this:
#users = filtered_users.joins(:posts).where('posts.created_at > ?', 1.week.ago)
FYI my answer was exactly what I thought it might have to be in the initial post. I'd love for there to be a more DRY solution, but I ended up doing something like this:
IN USER MODEL
def find_in_api_group
# NOTE settings.api_group is a string => "1,2,4"
if settings.api_group.nil? || settings.api_group.blank?
where("") # THERE HAS TO BE BETTER WAY OF SAYING THIS WITHOUT INTERRUPTING THE CHAIN
else
where("group_id IN (?)", settings.api_group)
end
end
IN VARIOUS CONTROLLERS
user = User.find_in_api_group
#then chain various error tests and additional activeRecord statement

Rails 3 ActiveRecord validation based on user permissions

I'm shifting code from an application built in a non-standard custom PHP framework into Ruby on Rails (version 3). In the PHP version all the controllers are really fat, with thin models, which I've always disagreed with, so I'm enjoying the way Rails does validation at the model level, which is probably 90% of what's happening in these fat controllers currently.
One problem I'm facing, and unsure how to resolve however, is that of differing validation rules based on who's making the change to the model. For example, an administrator, or the original creator of the record should be able to do things like flag a record as deleted (soft delete) whereas everybody else should not.
class Something < ActiveRecord::Base
...
validates :deleted, :owned_by_active_user => true
...
end
class OwnedByActiveUserValidator < ActiveModel::EachValidator
validate_each(record, attr_name, attr_value)
# Bad idea to have the model know about things such as sessions?
unless active_user.admin? || active_user.own?(record)
record.errors.add :base, "You do not have permission to delete this record"
end
end
end
Since the model itself is (in theory) unaware of the user who is making the change, what's the "rails way" to do this sort of thing? Should I set the active user as a virtual attribute on the record (not actually saved to DB), or should I just perform these checks in the controller? I have to admit, it does feel strange to have the model checking permissions on the active user, and it adds complexity when it comes to testing the model.
One reason I'm keen to keep as much of this as possible in the model, is because I want to provide both an API (accessed over OAuth) and a web site, without duplicating too much code, such as these types of permissions checks.
It is really the controller's job to handle authorization, or to delegate authorization to an authorization layer. The models should not know about, nor have to care about, who is currently logged in and what his/her permissions are - that's the job of the controller, or whatever auth helper layer the controller delegates that to.
You should make :deleted in-attr_accessible to mass assignment via new, create, or update_attributes. The controller should check the authenticated user's authorizations separately and call deleted= separately, if the authenticated user is authorized.
There are several authorization libraries and frameworks to help with authorization or to function as an authorization layer, such as cancan.
I would solve this with a before_filter in my controller, instead of with validations in my model.
class SomethingController < ApplicationController
before_filter :require_delete_permission, :only => [:destroy]
def destroy
# delete the record
end
private
def require_delete_permission
unless current_user.is_admin || record.owner == current_user
flash[:error] = 'You do not have delete permissions'
redirect_to somewhere
end
end
end
I have come across the same issue in Rails 2.3 and finally come up with this solution. In your model you define some atribute, depending on which you switch on/off validation. Than you your control you set this attribute depending on the date available to controller (such as user privileges in your case) as follows:
Class Model < ActiveRecord::Base
attr_accessor :perform_validation_of_field1 #This is an attribute which controller will use to turn on/off some validation logic depending on the current user
validates_presence_of :field1, :if => :perform_validation_of_field1
#This validation (or any similar one) will occur only if controller sets model.perform_validation_of_field1 to true.
end
Class MyController < ActionController::Base
def update
#item = Model.find(params[:id])
#item.update_attribute(params[:item])
#The controller decides whether to turn on optional validations depending on current user privileges (without the knowledge of internal implementation of this validation logic)
#item.perform_validation_of_field1 = true unless active_user.admin?
if #item.save
flash[:success] = 'The record has been saved'
redirect_to ...
else
flash.now[:error] = 'The record has not passed validation checks'
render :action => :edit
end
end
I think that in Rails 3 it can be done in similar manner.

Rails3 - Permission Model Before_Save Check?

I have a permission model in my app, that ties (Users, Roles, Projects) together.
What I'm looking to learn how to do is prevent a user for removing himself for their project...
Can you give me feedback on the following?
class Permission < ActiveRecord::Base
.
.
.
#admin_lock makes sure the user who created the project, is always the admin
before_save :admin_lock
def before_save
#Get the Project Object
project = Find(self.project_id)
if project.creator_id == current_user.id
# SOME HOW ABORT OR SEND BACK Not Allowed?
else
#continue, do nothing
end
end
end
Is that look like the right approach?
Also, I'm not sure how to do the following two things above:
How to abort prevent the save, and send back an error msg?
Get the devise, current_user.id in the model, that doesn't seem possible, so how do Rails gurus do stuff like the above?
Thanks for reading through
How to abort prevent the save, and send back an error msg?
return false during the callback chain tells activemodel to stop (similar to how adding errors to the model during a validation tells it to stop at that point)
self.errors.add_to_base "msg" will add an error to the model, which can then be rendered on the view.
Get the devise, current_user.id in the model, that doesn't seem possible, so how do Rails gurus do stuff like the above?
Models shouldn't really know about things like the current request, if at all possible, you should be locking things down at the controller/action level.
EDIT:
So, the role of controllers is to deal with everything involved in getting the correct information together based on the request, and passing it to the view (which becomes the response). People often say "make your models fat and your controllers skinny", but that could be said of any system that embraces object oriented design -- your logic should be in objects when possible.
That being said, the whole point of controllers is to deal with routing the right things to the right places, and authentication is definitely a concern of routing.
You could easily move the line comparing creator_id to user id in the action, and react based on that.
Now, sometimes you genuinely need that stuff in the model and there is no way around it. That becomes a problem, because you need to fight rails to get it there. One way would be to attr_accessor a current_user field on your model, and pass that in on initialize. Another would be to remove the fields from the params hash that a user is not allowed to change in the action. Neither is really that nice though.
Agreed with Matt that you should try to use the controller for the redirect. The model should have the logic to determine if the redirect is appropriate. Maybe something like
class ProjectsController < ApplicationController
def update
redirect_to(projects_url, :alert => "You can't remove yourself from this project.") and return if Role.unauthorized_action?(:update, params[:project])
#project = Project.find(params[:id])
if #project.update_attributes(params[:project])
...
end
class Role
def self.unauthorized_action?(action, params)
# your logic here
end
You should check out CanCan for some ideas.
In permission model take one field project_creater as boolean
In project modelbefore_create :set_project_ownership
def set_project_ownership
self.permissions.build(user_id: User.current.id, project_creater: true)
end
In project controllerbefore_filter :set_current_user
In Application controllerdef set_current_user
User.current = current_user
end

Resources