How to efficiently combine one link calling on two methods? - ruby-on-rails

A User can have multiple Organizations. In a session hash it is defined for which organization a user currently is logged in for (which can only be 1 organization). There's also a view that displays an overview with a table of organizations the user is part of. On this screen the user can switch to another organization, which resets the session hash to that organization. Simplified version (removed validations):
def set_session(organization)
session[:logged_in_for] = organization.id
end
def logged_in_organization
set_session(user.organizations.first) if session[:logged_in_for].nil?
#organization = Organization.find(session[:logged_in_for])
end
The overview screen also needs to contain other functions for each of the organizations (these functions are also available from other screens such as the profile of each organization, so their methods are already in place). An example could be the function to add a new user for the organization.
But these other function use logged_in_organization as the organization for which they perform the function (there are security reasons why I want to keep it that way).
Therefore, what I would like is that if the function in the overview screen is clicked it executes the set_session(organization) method before it executes the function:
executes set_session(organization) for that organization
executes for example add_user_path
Is this possible? I though about adding additional routes and methods. These would then first calls on set_session(organization) and after that on the function itself. But that would require quite a few additional routes and methods, and perhaps there's an easier way? (perhaps tell the link_to in the view to execute two methods?, although the link_to documentation does not seem to facilitate such)
Update: Would it be possible to add the second route as a param in the overview page; for example (other functions will have other syntax than organization_id: organization.id so that's not generic):
<%= link_to "Add user", move_on_path( organization, adduser_path(organization_id: organization.id) ) %>
With as move_on method:
def move_on(organization, url)
set_session(organization)
redirect_to url
end
Clicking the link currently produces an error pointing to def move_on(organization, url): wrong number of arguments (0 for 2). What am I doing wrong here? P.S. I have the following route:
get 'move_on' => 'user#move_on', as: 'move_on'

It seems to me that the simplest way to handle that is just to modify your logged_in_organization method:
def logged_in_organization
#organization ||= begin
set_session(user.organizations.first) if session[:logged_in_for].nil?
#organization = Organization.find(session[:logged_in_for])
end
end
That way it won't overwrite #organization if it's already set. And then you just set it in before_action of relevant controllers, like so:
before_action :set_organization
def set_organization
#organization = current_user.organizations.find(params[:organization_id])
end

Related

Ruby on Rails 7 Multistep form with multiple models logic

I am currently struggling with building up a multi step form where every step creates a model instance.
In this case I have 3 models:
UserPlan
Connection
GameDashboard
Since the association is like that:
An user has an user_plan
A connection belongs to an user_plan
A game_dashboard belongs to a connection
I would like to create a wizard to allow the current_user to create a game_dashboard going through a multi-step form where he is also creating connection and user_plan instance.
For this purpose I looked at Wicked gem and I started creating the logic from game_dashboard (which is the last). As soon as I had to face with form generating I felt like maybe starting from the bottom was not the better solution.
That’s why I am here to ask for help:
What would be the better way to implement this wizard? Starting from the bottom (game_dashboard) or starting
from the top (use_plan)?
Since I’m not asking help for code at the moment I didn’t write any controller’s or model’s logic, in case it would be helpful to someone I will put it!
Thanks a lot
EDIT
Since i need to allow only one process at a time but allowing multiple processes, to avoid the params values i decided to create a new model called like "onboarding" where i handle steps states there, checking each time the step
The simplest way would be to rely on the standard MVC pattern of Rails.
Just use the create and update controller methods to link to the next model's form (instead of to a show or index view)
E.g.
class UserPlansController < ApplicationController
...
def create
if #user_plan = UserPlan.create(user_plan_params)
# the next step in the form wizard process:
redirect_to new_connection_path(user_id: current_user, user_plan_id: #user_plan.reload.id)
else
#user_plan = UserPlan.new(user: current_user)
render :new
end
end
...
# something similar for #update action
end
For routes, you have two options:
You could nest everything:
# routes.rb
resources :user do
resources :user_plan do
resources :connection do
resources : game_dashboard
end
end
end
Pro:
This would make setting your associations in your controllers easier because all your routes would have what you need. E.g.:
/users/:user_id/user_plans/:user_plan_id/connections/:connection_id/game_dashboards/:game_dashboard_id
Con:
Your routes and link helpers would be very long and intense towards the "bottom". E.g.
game_dashboard_connection_user_plan_user_path(:user_id, :user_plan_id, :connection_id, :game_dashboard)
You could just manually link your wizard "steps" together
Pro:
The URLs and helpers aren't so crazy. E.g.
new_connection_path(user_plan_id: #user_plan.id)
With one meaningful URL variable: user_plan_id=1, you can look up everything upstream. e.g.:
#user_plan = UserPlan.find(params['user_plan_id'])
#user = #user_plan.user
Con:
(not much of a "con" because you probably wind up doing this anyway)
If you need to display information about "parent" records, you have to perform model lookups in your controllers first:
class GameDashboardController < ApplicationController
# e.g. URL: /game_dashboards/new?connection_id=1
def new
#connection = Connection.find(params['connection_id'])
#user_plan = #connection.user_plan
#user = #user_plan.user
#game_dashboard = GameDashboard.new(connection: #connection)
end
end

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

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.

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.

How do I respect RESTful methods when using find_or_initialize_by in Rails 3.2?

I have a resource in my project that collects some information from a user. Basically it's a form that they fill out before they can access another area of the site. It then sets a cookie for a week, but if they come back it will look up their previous entry and keep their preferences tied to them (and will update any details as long as the email address matches).
Currently I have a Applicants controller that looks like this:
class ApplicantsController < ApplicationController
...
def create
#applicant = Applicant.find_or_initialize_by_email(params[:applicant])
if #applicant.new_record? ? #applicant.save : #applicant.update_attributes(params[:applicant])
set_cookie_and_redirect
else
render 'new'
end
end
def update
if #applicant.update_attributes(params[:applicant])
set_cookie_and_redirect
else
render 'new'
end
end
end
The set_cookie_and_redirect is a private method that just sets some cookies and redirects the user to a page. The code works, but it just feels dirty. It's essentially updating a record within the create method under the condition that it's not a new record. I'm also forced to have an update method in case an existing record comes back with a validation error--the form helper will then switch the form over to sending to the update method.
So to my point... is there a more appropriate way to push the update_attributes call in the create method to the update method? Or better put, is there a better way to respect the RESTful methods in isolating the create and update functionality?
UPDATE: I wanted to be a little more specific too. If the user has filled this form out before it will set a cookie so they don't have to fill it out again for seven days. However after seven days the cookie is expired and they see the form again. The controller doesn't know if the user is new or existing until they add user input into the form which is then compared based on the email address.
Thanks in advance! I definitely look forward to anyone's thoughts on this.
The create method should only create, and the update method should only update. Let Rails decide which is going to happen based on what is inside of #applicant when the form is rendered - It essentially does what you're doing: Checks if the record is new or not, and sends it to update/create accordingly. Example:
def applicant
#applicant = Applicant.find_or_initialize_by_email(cookies[:email])
# renders applicant.html.erb form
end
<%= form_for #applicant do |f| %>
# ... fields ...
<% end %>
def create
#applicant = Applicant.new(params[:applicant])
#applicant.save
# .. etc.
end
def update
#applicant = Applicant.find_by_email(cookies[:email])
#applicant.update_attributes(params[:applicant])
# ... etc.
end
Rails will send the request to the correct action based on the new_record? status of the Applicant object.

Resources