Why Rails defaults creation to a POST on collection in HTML form? - ruby-on-rails

When generating a scaffold, by default the new_resource_path generates a form that will submit to resources_path.
This makes total sense in a RESTful mentality.
But, given that the generated material does not uses it as a REST resource, why does it POST to the collection path?
When the resource is successfully created, Rails will redirect to the created resource path. When any error occurs, Rails will render the new template that will present the errors (generated by scaffolding).
This seems fine, except that when any errors occurs when trying to create the resource, the URL will change to the collection path. This means that if user tries to refresh the page, it will not see the creation form. If the application does not allow listing for this resource, a routing error may happen. In case the application uses any type of authorization and the current user does not has the required authorization to list stuff, it may see a forbidden.
I see Rails scaffold generator as something the community agrees to be the standard way to do basic CRUD in it. So, why this behavior?
It seems that by keeping a purist RESTful resources approach we are breaking user experience a bit.
To see an example of this, just create a new Rails application, scaffold a new entity and try to create it with some validation errors.
$ rails new example
$ cd example
$ rails generate scaffold note text
# edit app/models/note.rb
class Note < ApplicationRecord
validates :text, length: { minimum: 10 }
end
$ rails db:migrate
$ rails server
# go to localhost:3000/notes/new
# click 'Create Note'
# see the error
# hit browser's refresh button
# now you are listing notes, and not creating one
If you think "this should not harm a real application". I've come up with this when writing tests for authentication.
My application is using Devise and fails for this test:
test 'new user should not be able to register with wrong password confirmation' do
email = 'newuser#newdomain.com'
password = 'little$secret'
password_confirmation = 'big$secret'
visit new_user_registration_path
fill_in 'Email', with: email
fill_in 'Password', with: password
fill_in 'Password confirmation', with: password_confirmation
assert_no_difference ->{ User.count } do
click_on 'Sign up'
end
assert page.has_content?("Password confirmation doesn't match Password")
# FAILS:
assert_equal new_user_registration_path, current_path
end
What this means in real life: When user tries to create an account, submit an invalid form, see the error and hit refresh, it is on an invalid path as the resource does not support listing (i.e. /users).
To make that last assertion pass, I had to overwrite the default Devise view to submit the form to /users/sign_up instead of just /users and to add a new route to call create when a POST is made to this URL. Then I realized that this will happen to any controller following the RESTful Resource approach, unless developers create this new route and use a custom URL for submitting creation forms.
Also, the "purist RESTful Resource approach" doesn't seem to be so purist. When you submit your form with invalid data, the POST will result in a 200 OK rendering an HTML with errors, instead of a 400 Bad Request. So, why not submit the form to the same URL the form exists in?
My bet is that I'm missing something, but I can't figure it out. So, what am I missing?

But, given that the generated material does not uses it as a REST
resource, why does it POST to the collection path?
So, why not submit the form to the same URL the form exists in?
Because the rails conventions embrace statelessness.
The form that you see when a create fails shows the result of a POST request. It is not meant to be repeated - or shared.
You could potentially have POST /notes/create and create a GET /notes/create route so that it would show the form after a refresh - but is that a good design from a framework point of view? I would say no.
Forms that POST back to the same URL can give a bad user experience - like the "Confirm form submission" dialog when you hit the back button. This is actually worse than the scenario you are painting up as it can lead to unexpected consequences for the user.
I see Rails scaffold generator as something the community agrees to be
the standard way to do basic CRUD in it.
The rails scaffold command is a rapid prototyping tool. They are not meant as the authoritative source of the "right" way to do rails nor does the community hold them as the word of god.
Also, the "purist RESTful Resource approach" doesn't seem to be so
purist.
The Rails community is not very purist. If anything its quite pragmatic and aims towards embracing concepts like REST but with a focus on developer convenience and "should just work".
When you submit your form with invalid data, the POST will
result in a 200 OK rendering an HTML with errors, instead of a 400 Bad
Request.
This is pragmatism, back in the day Internet Explorer would do all kinds of annoying things when given 4XX response codes. 200 OK guarantees the client will render the response - although it is tecnically wrong.

This seems fine, except that when any errors occurs when trying to
create the resource, the URL will change to the collection path. This
means that if user tries to refresh the page, it will not see the
creation form.
I don't get you : If you refresh the page, it will just re-POST the same parameters and so show the same form with errors. I just re-checked that.
If the application does not allow listing for this resource, a routing
error may happen. In case the application uses any type of
authorization and the current user does not has the required
authorization to list stuff, it may see a forbidden.
So, a user would not be allowed, for example, to view a list of posts, but it would allowed to create a new one ?

Related

Writing Cucumber tests that pass extra information

I have a Ruby on Rails program with feature tests in Cucumber.
I just implemented a feature where an admin can create a new password for a client-user. Now, on the "edit client" page, there's an additional button that allows the admin to set the password. Now, I just need to make a cucumber test.
I am trying to base this off of the normal test for client changing password, and the test for admin changing the user's information. What I have is this:
Feature: Analyst changes client's password
As an Analyst
I want to change client's password
So that I can reset the client's account
Background:
Given the following client accounts
| email | password |
| user1#someorg.com | password |
And I am logged in as an admin
#javascript
Scenario: Update a Client user
Given I navigate to the Clients Management Page
When I edit the Client User "user1#someorg.com"
And I click on "button"
Then I should be on the Clients Password Page
#javascript
Scenario: Can change password if confirmation matches
Given I navigate to the Clients Password Page
And I enter "Password1" as the password
And I enter "Password1" as the password confirmation
And I submit the form
Then I should be taken to the Client Landing Page
And The client's password should be "Password1"
In the steps, I have:
Given /^I navigate to the Clients Password Page$/ do
client_management_index_page = ClientsPasswordPage.new Capybara.current_session
client_management_index_page.visit
end
Then /^I should be on the Clients Password Page$/ do
client_password_page = ClientsPasswordPage.new Capybara.current_session
expect(client_password_page).to be_current_page
end
and ClientsPaswordPage:
class ClientsPasswordPage
include PageMixin
include Rails.application.routes.url_helpers
def initialize session
initialize_page session, edit_admin_client_password_path
end
end
except that edit_admin_client_password_path takes an :id, for the user who's being edited. I can't figure out how to get that information into it.
In case it matters, I'm using Devise for the security stuff...
There are a few ways to do this. The simplest is to realize that you're only creating one client during the test so
Client.first # whatever class represents clients
will always be that client. Obviously that doesn't work if you have tests where you create one more than client, so then you can create instance variables in your cucumber steps which get set on the World and can then be accessed from other steps and passed to your page objects
When I edit the Client User "user1#someorg.com"
#current_client = Client.find_by(email: "user1#someorg.com") # obviously would actually be a parameter to the step
...
end
Then /^I should be on the Clients Password Page$/ do
client_password_page = ClientsPasswordPage.new Capybara.current_session, #current_client
expect(client_password_page).to be_current_page
end
of course without the page object overhead this would just become
Then /^I should be on the Clients Password Page$/ do
expect(page).to have_current_path(edit_admin_client_password_path(#current_client))
end
There are a number of things you can do to simplify this scenario. If you have simpler scenarios, with simpler step definitions then it will be easier to solve implementation problems like how you get a client in one step to be available in a second step.
The main way to simplify scenarios is to not have anything at all in the scenario that explains HOW you have implemented the functionality. If you take all the clicking on buttons, filling in fields, and visiting pages out of your scenarios you can focus on the business problem.
So how about
Background
Given there is a client
And I am logged in as an admin
Scenario: Change clients password
When I change the clients password
Then the client should have a new password
Note: This immediately raises the question 'How does the client find out about there new password?', which is what good simple scenarios do, they make you ask valuable questions. Answering this is probably out of scope here.
Now lets have a look at the implementation.
Given 'there is a client' do
#client = create_new_client
end
When 'I change the clients password' do
visit admin_change_password_path(#client)
change_client_password(client: #client)
end
Just this might be sufficient to get you on the right path. In addition something like
Given 'I am logged in as an admin' do
#i = create_admin_user
login_as(user: #i)
end
would help.
What we have done here is
Push the HOW down your stack so that now the code you right to make this work is out of your scenarios and step definitions
Used variable to communicate between steps the line #client = create_new_client creates a global (actually global to Cucumber::World) variable that is available in all step definitions
You can create helper methods by adding modules to Cucumber world and defining methods in them. Note these methods are global so you have to think carefully about names (there are very good reasons why these methods are global). So
module UserStepHelper
def create_new_client
...
end
def create_admin_user
...
end
def change_client_password(client: )
...
end
end
World UserStepHelper
Will create a helper method you can use in any of your step definitions.
You can see an example of this approach here. A project I used for a talk at CukeUp 2013. Perhaps you could use this as your tutorial example.

Multiple Domain pointing to single rails app displaying different content with the same url path

I have searched around the web and there are answers that have helped me abit, however I am still stuck, so here goes.
I a Rails 4 app that allows users to create a biography/blog and then access it using their own domain.
Users can choose from several pre-made website templates (main page, about me page, my hobbies page, etc...), and then they load up their content using a CMS. The content will then be displayed using their chosen template when visitors visit their domain.
Eg:
User 1:
Domain: www.user1.com
Template: Template A
User 2:
Domain: www.user2.com
Template: Template B
Desired Results
When a visitor visits www.user1.com, they will see the main page. When they click on "About Me", they will be redirect to www.user1.com/about-me. If a visitor visits the "About Me" page for user 2, they will see www.user2.com/about-me.
My question here is, how do I set this up?
Based on this answer: Rails routing to handle multiple domains on single application
class Domain
def self.matches?(request)
request.domain.present? && request.domain != "mydomain.com"
end
end
------in routes.rb------
require 'subdomain'
constraints(Domain) do
match '/' => 'blogs#show'
end
I know I can route a different domain compared to mine to a separate controller, however, I need to route it to different template controllers which can change at any moment (users can change templates at will).
I know I can set up a general controller that can read incoming requests, then based on the hostname, I can extract the appropriate template and then redirect the request to that template's controller (eg: Template1Controller), however the url gets messed up, becoming something like "/template/template1/index" or "/template/template1/about-me" which is very bad and ugly. Furthermore, it will be extremely tricky to handle paths specific to only some templates (Template A might have a "My Resume" page while template B might have a "Family History" page instead).
Is there a way to do this?
I have thought about a method where I have a single controller that will handle everything (without redirects) and then just calls render template1/index, but I think it is a bad way of doing it (different template might need different data in each page).
Btw, this will be hosted on EC2.
EDIT
What I am looking to implement is quite similar to this question Mapping multiple domain names to different resources in a Rails app , but unfortunately no answers then. Im hoping 5 years later, someone might know how to get this done.
Thanks!
I do this pretty simple with Heroku. It's probably not hard anywhere.
Once you have DNS set up.. the Rails layer can look like...
Create a before_filter in ApplicationController. before_filter :domain_check
In my domain_check method I just have if request.host ~= /whatever/ do this elsif ... elsif ... end
"do this" can be a redirect or a render or whatever.

In rails 4.2, how to display a form for preview but ensure it cannot be submitted

I'd like to have a a form view that can, depending on circumstances, have submit functionality disabled in a bullet-proof way so that even a clever user could not edit the HTML source (via a browser extension) to re-add the submit button.
It seems one way to do that might be to somehow inject an invalid authenticity token that replaces the (valid) rails-generated one, so that even if a user somehow re-adds the submit button (by editing the HTML via a browser extension) it would still be an invalid submission.
My thought is to have some logic in the view:
- if #form_disabled # set by controller
- somehow_invalidate_the_authenticity_token?
How might one 'break' Rails form submission?
The purpose of doing this, instead of rendering the preview in a :show action, is to have the exact same view displaying both the live-form and the dead-form.
If I were you, I would use pundit.
It's pretty simple, and has few lines of code if you need to know how it works.
I'd start to write the code here, but I realize that the example at the readme fit your needs.
At the application controller add this
At the folder app/policies put the class PostPolicy, of course, you must replace "Post" with the name of your controller in singular (even if you have not a model with that name). The update? (and create?) actions should return true/false to indicate if user is allowed or not.
A few lines down on the readme, you will find the PostsController#update action, which call to authorize with the record before the update. I think you want do the same with create (then you need a create? method at the policy class).
Pundit needs current_user controller method, if you don't have it. Just follow the user customization instructions.
Of course, new and edit actions don't call authorize because they are allowed to everybody. Only the POST & the PUT/PATCH actions are forbidden.
Yes, it's more than a surgery of one line of code. But it's simple and the right way of give access to users.
After reading my other answer, I start thinking that you can do the same that Pundit does at the controller:
def update
if <unauthorized user>
flash[:alert] = "You are not authorized to perform this action."
redirect_to(request.referrer || root_path)
else
# all the update stuff
# ...
end
end

Is it safe to accept URL parameters for populating the `url_for` method?

I am using Ruby on Rails 4.1.1 and I am thinking to accept parameters (through URL query strings) that are passed directly to the url_for method, this way:
# URL in the browser
http://www.myapp.com?redirect_to[controller]=users&redirect_to[action]=show&redirect_to[id]=1
# Controller
...
redirect_to url_for(params[:redirect_to].merge(:only_path => true))
Adopting the above approach users can be redirected after performing an action. However, I think people can enter arbitraryparams that can lead to security issues...
Is it safe to accept URL parameters for populating the url_for method? What are pitfalls? What can happen in the worst case?
By logging params during requests to my application I noted Rails adds always :controller and action parameters. Maybe that confirms url_for can be used the above way since it is protected internally and works as-like Rails is intended to.
This it is safe internally as Ruby On Rails will only be issuing a HTTP redirect response.
As you are using only_path this will protect you from an Open redirect vulnerability. This is where an email is sent by an attacker containing a link in the following format (say your site is example.com).
https://example.com?foo=bar&bar=foo&redirect=http://evil.com
As the user checks the URL and sees it is on the example.com domain they beleive it is safe so click the link. However, if there's an open redirect then the user ends up on evil.com which could ask for their example.com password without the user noticing.
Redirecting to a relative path only on your site fixes any vulnerability.
In your case you are giving users control of your controller, action and parameters. As long as your GET methods are safe (i.e. no side-effects), an attacker could not use this by creating a crafted link that the user opens.
In summary, from the information provided I don't see any risk from phishing URLs to your application.
Rails redirect_to sets the HTTP status code to 302 Found which tells the browser to GET the new path as you defined it by url_for. GET is a considered a safe method in contrast to
... methods such as POST, PUT, DELETE and PATCH [which] are intended for
actions that may cause side effects either on the server, or external
side effects ...
The only problem would have been if someone could gain access to methods such as create and destroy. Since these methods use HTTP methods other than GET (respectively POST and DELETE) it should be no problem.
Another danger here is if you go beyond CRUD methods of REST and have a custom method which responses to GET and changes the database state:
routes.rb
resources something do
member do
get :my_action
end
end
SomethingController
def my_action
# delte some records
end
For future ref:
Rails has a number of security measurements which may also interest you.
It's not exactly an answer, just wanted to point out that you shouldn't use something like
url_for(params)
because one could pass host and port as params and thus the url could lead to another site and it can get worse if it gets cached or something.
Don't know if it threatens anything, but hey, it's worth pointing out

Authlogic run validations on login prior to create action

I need to run the built-in validations on the login field prior to actually creating the user record, is there a way to do this in Authlogic? The reason for is when a user types in a new login, AJAX is invoked to check and see that the login in unique, valid, etc. Once that is done, the user can enter his email to claim the login, it's a 2 step process.
The User model uses ActiveRecord validations, so this isn't specific to Authlogic. If you want to run the validations on a model you can call user.valid?. This will return true or false depending on if the entire model is valid. However it also fills up the user.errors object so you can then check if a given attribute is valid.
Here is some code that uses RJS to do the AJAX. But you can use anything and organize it however you want.
user = User.new(params[:user])
user.valid? # we aren't interested in the output of this.
error = user.errors.on(:login)
if error
page.insert_html :before, "user_login", content_tag(:span, error, :class => "error_message")
end
You may be interested in my Mastering Rails Forms screencast series where I cover this topic in the 2nd episode.

Resources