My Rails app depends on the Rails cache to temporarily hold user input and pass it between controller actions during the user log in process. However, I realized that I have made a pretty serious error (since I am super-new to Rails and MVC, in general) and could use some help/advice/wisdom - basically, if two users are simultaneously (or nearly simultaneously) submitting data and going through the controller steps below, there's going to be some trouble - lost data, one user's data being entered as another, etc. When I was building this, I did not understand the nature of controllers, though I for sure have a better idea now....
In short, here's how the code below is intended to work: a non-logged-in user fills out a form and submits that data which is passed to the submission action and held in a Rails cache object called incoming_report; the user is then prompted to log in (via Devise), and a successful log in triggers the approval controller action which reads the incoming_report cache object, saves that data to the database, and then creates a new cache object called ids to hang on to the id #'s of the user's entries; this redirects to the summary action which gathers up those ids and shows the corresponding data to the user as a summary of their entry.
report_controller.rb
#ON SUBMITTING THE FORM...
def submission
#incomingReport = ActiveSupport::JSON.decode(params[:report])
#SUBMITTED DATA IS HELD IN THE CACHE AS 'incoming_report' TO PASS TO approval CONTROLLER ACTION ON SUCCESSFUL LOGIN
Rails.cache.write("incoming_report",#incomingReport)
end
#ON SUCCESSFUL LOG IN...
def approval
#incomingReport = Rails.cache.read("incoming_report")
#newReportIDArray = Array.new
#incomingReport.each do |x|
#DATA PROCESSING STUFF HERE...
end
#DELETE 'incoming_report' FROM CACHE
Rails.cache.delete("incoming_report")
#ID NUMBERS OF ENTRIES ARE HELD TO PASS TO summary CONTROLLER ACTION
Rails.cache.write("ids",#newReportIDArray)
redirect_to report_summary_path
end
#AFTER DATA IS ENTERED...
def summary
#newReportIDs = Rails.cache.read("ids")
#DELETE 'ids' FROM CACHE
Rails.cache.delete("ids")
end
For the most part, this works very well, but if users cross each other during any of these actions, it's disastrous. For instance, if one user hits the submission action while another user is busy logging in, the incoming_report object of the second user will overwrite that of the first user... and so on and so forth.
The main source of this problem is the fact that users log in after they have clicked the form submit button - the app must be structured that way - and the data can't be held in local/session storage objects because it includes base64 strings and is too long to employ those methods in certain browsers. Holding this data on the server seemed like the best course of action until I noticed this issue...
Is there any way that I can prevent users from getting in each other's way in the controllers? Or have I made a pretty fatal error?
Editing to show solution
Ultimately, using session variables instead of the Rails cache saved the day. Here's the reconfigured controller to show the solution...
def submission
#incomingReport = ActiveSupport::JSON.decode(params[:report])
#newReportIDArray = Array.new
#incomingReport.each do |x|
#DATA PROCESSING STUFF HERE
#newReportIDArray.push(#new_report.id)
end
session[:ids] = #newReportIDArray
respond_to do |format|
format.json do
render json: {
success: 200
}.to_json
end
end
end
def approval
#reportIDs = session[:ids]
#reportIDs.each do |x|
#new_report = Report.find(x)
#new_report.user_id = current_user.id
#new_report.save
end
redirect_to report_summary_path
end
def summary
#reportIDs = session[:ids]
end
Many thanks to the two folks who helped me on this and showed me the ways of the session variable.
I ran into a problem with the session variable being 'lost' between controller actions - Rails / Devise - updating session variables between controller actions
I would suggest that you store the initial information into the database. From there I would either save the id in a cookie or session variable. You would then be able to retrieve the information from the database in the approval controller. Another option I would consider would be to generate a guid as an id and pass that along to the sign in page as a query string parameter. Store the guid in a hidden input tag, under the form tag. Then when the user submits their credentials, you can also pass the guid. At that point you log them in. If successful, pull the guid from the posted data and then retrieve the database information with the guid.
Wade
Related
I am working on a module where I have to take the consent of the user to save the set of records.
those set of records are created in an action, which has to be made available in another action of the same controller, the records are being saved by the user consent.
now I can send these set of records to UI, from UI to again controller, if the user continues to save, if not cancel.
Problem is there will be thousands of records, which is painful to carry between UI and controller so My plan is to make the set of records available to the action which is being called by the continue button
the code
def create
#valid_members = generate_member_upload_results(params[:member_upload_user][:members_list])
end
in this action #valid_members is going to have the set of records. after this action executes in UI we will ask user whether the records are to be saved if no then cancels if yes then the following action will takes palce
def create_member
count = 0
unless #valid_members.blank?
#valid_members.each do |m|
count = count + 1
m.save(:validate => false)
end
end
redirect_to :back , notice:'#{count} members records created'
end
I want my #valid member should the same object which I used in create def.
I'm not entirely sure this is feasible with the flow you're suggesting. This sounds like something that could be resolved with a multi-step form but you would need to pass the data across or temporarily store it, which is seemingly what you're trying to avoid.
Alternatively, can you create a Rails endpoint that services the first step via javascript directly from the frontend? That can return the data without the user leaving the page, they can then confirm they are happy and submit the page once with approval.
I've got a number of security concerns about my current application and wondering if I am leaving myself open to abuse, in the following arenas.
a) .My main access control method is by maining a current_user, current_company current_project method in my application controller. These methods return object based on stored session keys established when a user logs in and cleared when they log out. I.e if I want to know something about the current user, I can call "current_user.role" or if I want see whether the account a user is trying to change belongs to him, I check whether the associated account id which is requested in the url actually belongs to that user, essentially as follows
in Account controller
def account_info
redirect_to login_path if !user.logged_in
account_id=params[:account_id]
#account = Account.find(account_id)
unless account_belongs_to_user(account_id)
redirect_to unauthorized_path
end
end
In my application controller, when a user is initially authenticated, I do something like this:
session[:current_user_id] = user.id
and clear that session key when the user logs out.
Then when account is requested, and account_belongs_to_user is called, the application controller processes it, more or less like this:
def account_belongs_to_user(account_id)
account = Account.find(account_id)
return account.user_id==session[:current_user_id]
end
So I guess my security scheme ultimately relies on whether the session data is secure and not trivially spoofable.
b) When I render pages I sometimes pass objects which have senstive data to my erb pages to generate the page text.
For example, I might pass a "company" object (ActiveRecord) to the view to generate an invoice screen. But the company object, passed as #company, has a lot of sensitive data like access keys and the like. Not really being fully aware of the the internals, if I don't specifically include something like:
<%= #company.access_token %>
on my web page, can I be confident that the attributes of #company won't somehow be passed into the browser unless I specifically ask for them to be rendered on the page?
This is obviously an issue when using rails to serve data for say, AngularJS single page applications, as everything I pass for Angular to render the page I assume is probably accessible to an evil-doer even if not on the page itself, but I'm hoping that's not the case with pages generated server side by rails.
This may be a naive question, but thanks as I just want to be certain what I am doing before start spilling secrets all over the place.
put an authentication for the token using active_record callback
https://guides.rubyonrails.org/active_record_callbacks.html
I have a form on a rails view that submits data to a page that will represent a shopping cart summary page.
When I submit the data to the next page they are transmitted as follows according the console output.
"team"=>{"team_name"=>"Joe Blogs", "email"=>"joe#bloggs.com", "player1"=>"
3", "player2"=>"4", "player3"=>"5"}
I want to store this data is a session variable namely as a hash so if another team gets submitted to the summary page I can add it to the session as another hash entry. i.e. team[1], team[2].
Then I can access team[1].team_name, etc. and use it accordingly.
In summary, I want a user to be able to fill out a form and have it put into their cart. They can then go back and do the same again. Finally the can look at their cart and remove any records they don't want, clear the cart or submit what they choose into the database.
I can't find out how to do it or if it's even possible.
Any solutions or suggestions on how to implement this?
You can easily store a hash in Rails session.
Example:
class SomeController < ApplicationController
def some_action
session[:cart] = {"team_name"=>"Joe Blogs", "email"=>"joe#bloggs.com", "player1"=>"3", "player2"=>"4", "player3"=>"5"}
end
end
But, by default, Rails stores sessions in cookies, and a cookie size is limited to just 4 kilobytes of data, so if your hash is going to contain more than a few keys, you will need to use something else for session storage, e.g. the database.
To store session in the database you can use the activerecord-session_store gem.
I am having an issue with my Rails app, that if a user clicks submits on a form, then stops the browser from proceeding (KILL_CURSOR), if they resubmit the form it will create a duplicate entry in the database. I've tried installing javascript to disable input after submit, but it doesn't work in this use case.
Is there a way for me to validate whether a form is a resubmit even it it has the same authenticity token?
Here is my controller:
def create
#location = Location.new(params[:location])
#location.user_id = current_user.id
if #location.save
#refresh window to show new location
render :js => "window.location.reload();"
#check that form fields have been completed properly
else
respond_to do |format|
format.js
end
end
First, it is a good idea to disable the submit button using javascript. Not only this meets your need, but also it gives users a good feedback of their actions. But you can't rely on that, because the javascript might be disabled in web browser.
You could check the database to see whether there's already a duplicated document before inserting a new one. The point is that the definition of duplication is based on your business logic. For example, two requests about the same location from the same user in 5 minutes are duplicated. However, in extreme cases, two or more concurrent requests may lead to duplicated documents, because they may check the database at the same time, seeing nothing, then insert.
In the database layer, unique index in MongoDB could be helpful if some fields of data themselves defines the duplication and should be unique. For example, a given user at a given location should occur at most once in database. Building a unique index (user_id, location._id) can guarantee that constrain.
db.collection.ensureIndex( { user_id: 1, location._id: 1 }, { unique: true } )
Note unique index is not allowed in sharding unless it is the shard key. Because there is no easy way to guarantee that among many machines, if it's not the shard key.
I am implementing a payment gateway in my app.
Its like this:
The user fills the form with necessary details, along with a field containing return_url(say http://myapp.com/ebs_payment/ebs_response?dr={somedata}) and submit the form to a secure payment site. After the transaction is complete, the secure site puts some encrypted data into my param {dr} and the user is redirected back to the return url. The problem here is, when the user returns to the app with the return_url, the application fails to pick up the session data and returns a nil value.
Before submitting the form, I put the object #fdPaymentDets in to session.
Here is my controller:
class EbsPaymentController < ApplicationController
#before_filter :login_required,:check_user_authenticate
#access_control [:ebs_response] => ('user')
def ebs_response
#fdPaymentDets = session["fd_payment_details"]
#deal = Deal.find(#fdPaymentDets.deal_id)
#categories = find_all_categories
end
private
def find_all_categories
#sp = SitePreference.find(:first)
Category.find(:all,:limit => #sp.categories_display_limit,:order => 'position')
end
end
When the user is redirected to the return url (http://myapp.com/ebs_payment/ebs_response?dr={encrypted_data}) from the secure site, rails is not picking the #fdPaymentDets object from session and making it nil thus resulting in an error when accessing data from the nil object.
The surprising thing is that, when I put the same return_url in my browser by hand, the session data is neatly picked and everything goes well.
Am missing any thing here? What could be the obvious reason?
Any help appreciated.
Cookies and redirects are messy and bug prone (from a browser's implementation perspective).
Take a look at
Safari doesn't set Cookie but IE / FF does
Suggestion would be to change the implementation to set the session first in the show action, and then update the value before the redirect