Modify attribute value based on user role - ruby-on-rails

I have a column 'created_by' in a table. Based on the role of logged in user I have to show either the id of the user or the role of the user. Suppose the id logged in user is a customer and the record was created by the user who has support role, then it should show 'Support', and if the support logs in then show the id of the person who added(even if its a different support person's id). I can figure out the current user's role. How can I achieve this without defining a separate apis based on role? Is it possible to have same api to get results from db based on query but transform the column based on role.

My initial thought is to create a view helper.
I'll give you an idea for what it could look like. Since you didn't share the name of the model in your question, I'm picking an arbitrary model (Watermelons) that has a created_by relationship to Users. I realize that's a silly choice. I'm an enigma...
app/helpers/watermelons_helper.rb
module WatermelonsHelper
def created_by_based_on_role(watermelon)
if current_user.role == "Support" || watermelon.created_by.role == "Generic User"
watermelon.created_by.name
else
"The Watermelons.com Support Team"
end
end
end
app/controllers/watermelons_controller.rb
class WatermelonsController < Application Controller
def show
#watermelon = Watermelon.find(params[:watermelon_id])
end
end
app/views/watermelons/show.html.erb
...
<p>This watermelon was created by <%= created_by_based_on_role(#watermelon) %></p>
...
The reason why I'd make this a helper method and not a method in the watermelon.rb model is that the method is dependent on the current_user, which the model can't explicitly know each time (assuming you're using Devise or a hand-rolled, Hartl-style current_user helper method as well).
Edit: Per feedback, let's make it a model method...
app/models/watermelons.rb
class Watermelon < ActiveRecord::Base
...
def created_by_based_on_role(viewing_user)
if viewing_user.role == "Support" || self.created_by.role == "Generic User"
self.created_by.name
else
"The Watermelons.com Support Team"
end
end
...
end
Note that I'm implementing this as an instance method, which means you will need to call "watermelon.created_by_based_on_role(current_user)" in the controller or view.

Related

Check object type in Rails controller before calling method

I'm wondering if there is a more object-oriented way to accomplish what I'm doing. A controller in my app has a 'vote' method, which allows a user to cast an up or down vote on a resource. In order for a resource to gain voting functionality, it has to include one of two modules: Votable or DistrictVotable.
The DistrictVotable module means that you need to specify a district when voting. With the Votable module, that is not the case, you can vote without a district.
So, here is what the relevant part of my controller method looks like:
def vote
#resource = find_resource
vote_type = params[:vote_type].to_i
if #resource.is_a? DistrictVotable
#resource.vote(district, vote_type, current_user)
elsif #resource.is_a? Votable
#resource.vote(vote_type, current_user)
end
end
To me, it seems less than ideal that the controller needs to check the #resource type before calling vote, but I can't figure out away around this since that determines whether or not a district needs to be passed in.
This vote method is added to the controller by a VotableController module. Maybe I need to create a separate DistrictVotableController so the type check won't be needed?
You could make the vote method accept arbitrary number of arguments. One way to do it is:
In Votable module:
def vote(args)
user = args.fetch(:user)
vote_type = args.fetch(:vote_type)
# some logic
end
In DistrictVotable module:
def vote(args)
user = args.fetch(:user)
vote_type = args.fetch(:vote_type)
district = args.fetch(:district)
# some logic
end
And you call it with
#resource.vote(user: current_user, vote_type: params[:vote_type], district: district)
The district parameter will be ignored in the Votable module.

what is the "rails way" for enabling an admin to create privileges for an existing user?

I'm writing a ruby on rails website for the first time. I have a User model and a Manager model. The user has_one Manager and a Manager belongs_to a User. The Manager model contains more info and flags regarding privileges. I want to allow an admin while viewing a User (show) to be able to make him a manager.
This is what I wrote (probably wrong):
In the view: <%= link_to 'Make Manager', new_manager_path(:id => #user.id) %>
In the controller:
def new
#user = User.find(params[:id])
#manager = #user.build_manager
end
resulting in a managers/new?id=X Url.
I would separate roles and permissions from the User class. Here's why:
Managers are users too. They share the same characteristics of Users: Email address, first name, last name, password, etc...
What if a manager also has a higher level manager? You'll have create a ManagerManager class, and that's terrible. You might end up with a ManagerManagerManager.
You could use inheritance, but that would still be wrong. Managers are users except for their title and permissions, so extract these domains into their own classes. Then use an authorisation library to isolate permissions.
You can use Pundit or CanCan. I prefer Pundit because it's better maintained, and separates permissions into their own classes.
Once you have done that, allowing a manager to change a normal user to a manager becomes trivial and easy to test:
class UserPolicy
attr_reader :user, :other_user
def initialize(user, other_user)
#user = user
#other_user = other_user
end
def make_manager?
user.manager?
end
end
In your user class you can have something like:
def manager?
title == 'manager?'
# or
# roles.include?('manager')
# Or whatever way you choose to implement this
end
Now you can always rely on this policy, wherever you are in the application, to make a decision whether the current user can change another user's role. So, in your view, you can do something like this:
- if policy(#user).make_manager?
= link_to "Make Manager", make_manager_path(#user)
Then, in the controller you would fetch the current user, and the user being acted upon, use the same policy to otherwise the action, and run the necessary updates. Something like:
def make_manager
user = User.find(params[:id])
authorize #user, :make_manager?
user.update(role: 'manager')
# or better, extract the method to the user class
# user.make_manager!
end
So you can now see the advantage of taking this approach.

I'm attempting to determine my user's role from within a view helper and am concerned that I'm doing it wrong

I have helpers that are used to determine whether a given user is an administrator or a vendor. From within the helpers, I query the database for their respective role objects (administrator, vendor, etc.) for comparison against roles associated with the user but have a feeling that this is an ass-backward way to go about determine a user's role.
Am I doing something wrong here? What could I do better? I should probably mention that I'm using/learning Pundit, so maybe it contains a better means by which to accomplish this.
Here's my code:
users_helper.rb
1 module UsersHelper
2 def admin?
3 # Determine whether the user has administrator status within a given event
4 #admin_role = Role.find(1)
5 return true if #user.roles.include? #admin_role
6 end
7
8 def vendor?
9 # Determine whether the user is an approved vendor within a given event
10 #vendor_role = Role.find(2)
11 return true if #user.roles.include? #vendor_role
12 end
13 end
Here's how I use the helpers from within my template:
show.html.erb
1 <% provide(:title, #user.username) %>
2
3 <% if admin? %>
4 <p>Admin</p>
5 <% elsif vendor? %>
6 <p>Vendor</p>
7 <% else %>
8 Something else.
9 <% end %>
The associations of User model with roles imply that your User model may have more then one role such as admin or vendor at the same time.
I would suggest to refactor User model to use has_one association with role, so that users will have only one role.
Then the helper in user_helper.rb might be something like:
def is_a?(user)
roles = { Roles.find(1) => "Admin", Roles.find(2) => "Vendor"}
roles.fetch(user.role) do
"Something else."
end
end
This will return string with correct string definition of role. You can make comparison with that or use it in view as follows:
<p><%= is_a?(#role) %></p>
Which does same thing as your code above in one line.
This might be better over at Code Review.
1) Your role implementation is a bit clunky (what with the magic numbers) - do you do anything with the Role class that warrants its being an ActiveRecord class?
If not, you could use the role_model gem (as mentioned by Sontya), which saves the user roles as a bitmask in the users table.
In case you need a more sophisticated role class, you may want to have a look at rolify, which is similar in concept to your current solution.
Either way, user roles in Rails applications are a solved problem, you do not need to write your own solution (of course, don't let that discourage you, building your own solution is at the very least a good learning exercise).
2) Checking for roles in a view is a pretty bad idea, especially if you're already using pundit.
If you ever add a new role, you need to go through all the views and change the if conditions to support the new new role.
This is a problem that explodes when both your number of views and your number of roles grow.
Instead, use the pundit policies in your view, and check for the users role(s) in the corresponding policy object.
Example:
class CompanyPolicy < ApplicationPolicy
def delete?
user.is?(:admin)
end
end
in the view:
<% if policy(#company).delete? %>
delete link here
<% end %>
Don't forget to authorize in the controller as well!
That way, if you add a new role, you simply have to change a few policy objects and you're good to go.
First off all, include? method returns boolean, so:
return true if #user.roles.include? #admin_role
is equal to:
#user.roles.include? #admin_role
Unless you need nil instead of false.
Second, your method names and the code itself should be so simple and self-descriptive, to not require comments. Unless you need automated documentation, when writing public gem or something.
Regarding you question, IMHO you should do this in your User model:
def admin?
roles.where(id: 1).any?
end
Then you can do #user.admin? in the view.

Ruby on Rails security vulnerability with user enumeration via id

With Ruby on Rails, my models are being created with increasing unique ids. For example, the first user has a user id of 1, the second 2, the third 3.
This is not good from a security perspective because if someone can snoop on the user id of the last created user (perhaps by creating a new user), they can infer your growth rate. They can also easily guess user ids.
Is there a good way to use random ids instead?
What have people done about this? Google search doesn't reveal much of anything.
I do not consider exposing user IDs to public as a security flaw, there should be other mechanisms for security. Maybe it is a "marketing security flaw" when visitors find out you do not have that million users they promise ;-)
Anyway:
To avoid IDs in urls at all you can use the user's login in all places. Make sure the login does not contain some special characters (./\#? etc.), that cause problems in routes (use a whitelist regex). Also login names may not be changed later, that can cause trouble if you have hard links/search engine entries to your pages.
Example calls are /users/Jeff and /users/Jeff/edit instead of /users/522047 and /users/522047/edit.
In your user class you need to override the to_param to use the login for routes instead of the user's id. This way there is no need to replace anything in your routes file nor in helpers like link_to #user.
class User < ActiveRecord::Base
def to_param
self.login
end
end
Then in every controller replace User.find by User.find_by_login:
class UsersController < ApplicationController
def show
#user = User.find_by_login(params[:id])
end
end
Or use a before_filter to replace the params before. For other controllers with nested resources use params[:user_id]:
class UsersController < ApplicationController
before_filter :get_id_from_login
def show
#user = User.find(params[:id])
end
private
# As users are not called by +id+ but by +login+ here is a function
# that converts a params[:id] containing an alphanumeric login to a
# params[:id] with a numeric id
def get_id_from_login
user = User.find_by_login(params[:id])
params[:id] = user.id unless user.nil?
end
end
Even if you would generate random INTEGER id it also can be compromted very easy. You should generate a random token for each user like MD5 or SHA1 ("asd342gdfg4534dfgdf"), then it would help you. And you should link to user profile with this random hash.
Note, this is not actually the hash concept, it just a random string.
Another way is to link to user with their nick, for example.
However, my guess is knowing the users ID or users count or users growth rate is not a vulnerability itself!
Add a field called random_id or whatever you want to your User model. Then when creating a user, place this code in your UsersController:
def create
...
user.random_id = User.generate_random_id
user.save
end
And place this code in your User class:
# random_id will contain capital letters and numbers only
def self.generate_random_id(size = 8)
alphanumerics = ('0'..'9').to_a + ('A'..'Z').to_a
key = (0..size).map {alphanumerics[Kernel.rand(36)]}.join
# if random_id exists in database, regenerate key
key = generate_random_id(size) if User.find_by_random_id(key)
# output the key
return key
end
If you need lowercase letters too, add them to alphanumerics and make sure you get the correct random number from the kernel, i.e. Kernel.rand(62).
Also be sure to modify your routes and other controllers to utilize the random_id instead of the default id.
You need to add a proper authorization layer to prevent un-authorized access.
Let us say you you display the user information in show action of the Users controller and the code is as shown below:
class UsersController < ActionController::Base
before_filter :require_user
def show
#user = User.find(params[:id])
end
end
This implementation is vulnerable to id guessing. You can easily fix it by ensuring that show action always shows the information of the logged in user:
def show
#user = current_user
end
Now regardless of what id is given in the URL you will display the current users profile.
Let us say that we want to allow account admin and account owner to access the show action:
def show
#user = current_user.has_role?(:admin) ? User.find(params[:id]) : current_user
end
OTH authorization logic is better implemented using a gem like CanCan.

Rails plugin for Group of users

My Rails application have a User model and a Group model, where User belongs to a Group. Thanks to this, a user can be a admin, a manager, a subscriber, etc.
Until recently, when for example a new admin need to be create on the app, the process is just to create a new normal account, and then an admin sets the new normal account's group_id attribute as the group id of the admin... using some condition in my User controller. But it's not very clean, I think. Because for security, I need to add this kind of code in (for example) User#update:
class UsersController < ApplicationController
# ...
def update
#user = User.find(params[:id])
# I need to add some lines here, just as on the bottom of the post.
# I think it's ugly... in my controller. But I can not put this
# control in the model, because of current_user is not accessible
# into User model, I think.
if #user.update_attributes(params[:user])
flash[:notice] = "yea"
redirect_to root_path
else
render :action => 'edit'
end
end
# ...
end
Is there a clean way to do it, with a Rails plugin? Or without...
By more clean, I think it could be better if those lines from User#update:
if current_user.try(:group).try(:level).to_i > #user.try(:group).try(:level).to_i
if Group.exists?(params[:user][:group_id].to_i)
if Group.find(params[:user][:group_id].to_i).level < current_user.group.level
#user.group.id = params[:user][:group_id]
end
end
end
...was removed from the controller and the application was able to set the group only if a the current user's group's level is better then the edited user. But maybe I'm wrong, maybe my code is yet perfect :)
Note: in my User model, there is this code:
class User < ActiveRecord::Base
belongs_to :group
attr_readonly :group_id
before_create :first_user
private
def first_user
self.group_id = Group.all.max {|a,b| a.level <=> b.level }.id unless User.exists?
end
end
Do you think it's a good way? Or do you process differently?
Thank you.
i prefer the controller methods to be lean and small, and to put actual model logic inside your model (where it belongs).
In your controller i would write something along the lines of
def update
#user = User.find(params[:id]
if #user.can_be_updated_by? current_user
#user.set_group params[:user][:group_id], current_user.group.level
end
# remove group_id from hash
params[:user].remove_key(:group_id)
if #user.update_attributes(params[:user])
... as before
end
and in your model you would have
def can_be_updated_by? (other_user)
other_user.try(:group).try(:level).to_i > self.try(:group).try(:level).to_i
end
def set_group(group_id, allowed_level)
group = Group.find(group_id.to_i)
self.group = group if group.present? && group.level < allowed_level
end
Does that help?
Well if you have a User/Groups (or User/Roles) model there is no other way to go than that you have underlined.
If it is a one-to-many association you can choose to store the user group as a string and if it is a many-to-many association you can go for a bitmask but nonetheless either through business logic or admin choice you need to set the User/Group relation.
You can have several choices on how to set this relationship in a view.
To expand your model's capability I advice you to use CanCan, a very good authorization gem which makes it super easy to allow fine grain access to each resource in your rails app.

Resources