As your application now stands, anybody, whether they’re signed in or not, can create new projects. As you did for the actions in the TicketsController, you must restrict access to the actions in the ProjectsController. The twist here is that you’ll allow only a certain subset of users—users with one particular attribute set in one particular way—to access the actions.
You’ll track which users are administrators by putting a boolean field called admin in the users table. This is the most basic form of user authorization, which is not to be confused with authentication, which you implemented in chapter 6. Authentication is the process users go through to confirm their identity, whereas authorization is the process users go through to gain access to specific areas.
To restrict the creation of projects to admins, you alter the existing Background in features/creating_projects.feature and insert the following listing as the first three lines.
Given there are the following users: | email | password | | [email protected] | password | And I am signed in as them
This listing creates a user. The Background should now look like the following listing.
Given there are the following users: | email | password | admin | | [email protected] | password | true | And I am signed in as them Given I am on the homepage When I follow "New Project"
There’s a problem here: the admin attribute for User objects isn’t mass-assignable. You saw this issue in chapter 6 when the attr_accessible method was introduced. This restriction means that you can’t assign the admin attribute along with other attributes using the new, build, create, or update_attributes method.
You have to set this attribute manually by using either update_attribute or the setter, user.admin = [value]. You use the latter here, so change this step in features/step_definitions/user_steps.rb
Given /^there are the following users:$/ do |table| table.hashes.each do |attributes| unconfirmed = attributes.delete("unconfirmed") == "true" @user = User.create!(attributes) @user.confirm! unless unconfirmed end end
to this:
Given /^there are the following users:$/ do |table| table.hashes.each do |attributes| unconfirmed = attributes.delete("unconfirmed") == "true" @user = User.create!(attributes) @user.update_attribute("admin", attributes["admin"] == "true") @user.confirm! unless unconfirmed end end
If you pass the admin attribute in your table, it’ll be a string. You check whether the string is equal to true, and if it is, you use update_attribute to set the admin field manually to true or false, depending on whether or not attributes["admin"] is true.
When you run this feature, it can’t find the admin field for your users table, because you haven’t added it yet:
Given there are the following users: | email | password | admin | | [email protected] | password | true | undefined method `admin=' for #<User: ...>
You can generate a migration to add the admin field by running rails generate migration add_admin_to_users admin:boolean. You want to modify this migration so that when users are created, the admin field is set to false rather than defaulting to nil. Open the freshly generated migration and change this line
add_column :users, :admin, :boolean
to this:
add_column :users, :admin, :boolean, :default => false
When you pass in the :default option here, the admin field defaults to false, ensuring that users aren’t accidentally created as admins.
The command rake db:migrate db:test:prepare runs the migration, adds the admin field to the users table, and sets up the test database. Now you see that the step is passing:
Given there are the following users: | email | password | admin | | [email protected] | password | true |
With this step definition implemented, run rake cucumber:ok and rake spec to make sure you haven’t broken anything. According to this output, you haven’t:
16 scenarios (16 passed) 152 steps (152 passed)
Great! Now you can go about restricting the acts of creating, updating, and destroying projects to only those users who are admins.
For this step, you implement a before_filter that checks not only whether the user is signed in but also whether the user is an admin.
Before you write this before_filter, you write a controller spec rather than a Cucumber feature to test it. Cucumber features are great for defining a set of actions that a user can perform in your system, but controller specs are much better for quickly testing singular points, such as whether or not a user can go to a specific action in the controller. You used this same reasoning back in chapter 4 to test what happens when a user attempts to go to a project that doesn’t exist.
You want to ensure that all visits to the new, create, edit, update, and destroy actions are done by admins and are inaccessible to other users. Open spec/controllers/projects_controller_spec.rb, and add a let inside the describe so the top of the spec looks like the following listing.
describe ProjectsController do let(:user) do user = Factory(:user) user.confirm! user end context "standard users" do it "cannot access the new action" do sign_in(:user, user) end end ... end
Here you use a multilined block for the let method. This method defines a user method, which returns a newly created and confirmed user. You create this user using Factory Girl.
You then use this object in your test to sign in as that user. The benefit of using let over defining an instance variable in a before block is that the let code is called only when it’s referenced, whereas all the code in a before is evaluated regardless. This is helpful if some of your tests don’t need a User object.
Underneath the let, you add a short placeholder test that signs in as the user, attempting to use the userlet method. With this test, the let block is called, and you should get an error when you run this spec using bin/rspec spec/controllers/projects_controller_spec.rb:
Not registered: user (ArgumentError)
Therefore, you should create a user factory that creates a user object with a random email address (because you may wish to create more than one user at a time using this factory), and the password should default to password. Define this factory in a new file called factories/user_factory.rb:
Factory.define :user do |user| user.sequence(:email) { |n| "user#{n}@ticketee.com" } user.password "password" user.password_confirmation "password" end
In this factory, you use the sequence method provided by Factory Girl, which passes a unique number to the block and makes your user’s email addresses unique.
The files within the factories directory aren’t yet required by RSpec, so their factories aren’t available for you. To make RSpec load them, create a new file at spec/support/factories.rb and put this content in it:
Dir[Rails.root + "factories/*.rb"].each do |file| require file end
When the value of the email method is inside a block, it’s evaluated every time this factory is called and generates a new (hopefully random) email address.
The next bit of code to write is in your spec and is the first example to ensure that users who are not admins (such as the user object) cannot access the new action. The code in the following listing should replace the placeholder "cannot access the new action" example you’ve already got.
context "standard users" do it "cannot access the new action" do sign_in(:user, user) get :new response.should redirect_to(root_path) flash[:alert].should eql("You must be an admin to do that.") end end
This spec is placed inside a context block because you’ll have specs for standard users later. context blocks function similarly to describe blocks; they’re mainly used to specify the context of an example rather than to describe what it does.
On the first line of the example, you use the sign_in method, which is available in Devise, but the appropriate part isn’t yet included. This method takes two arguments: the scope and the resource. Because you have only one scope for Devise, you don’t have to worry about what this means right now. What you do care about is that this method will sign in a user.
To add this method, you must include a module in your RSpec configuration: the Devise::TestHelpers module. Open spec/spec_helper.rb, and you’ll see the RSpec.configure block (comments stripped) shown in the following listing.
RSpec.configure do |config| config.mock_with :rspec end
This configure method is responsible for setting up RSpec. To include the Devise::TestHelpers method, you could use it like this:
RSpec.configure do |config| config.mock_with :rspec config.include Devise::TestHelpers end
But you may want to rerun the RSpec generator to update your spec/spec_helper.rb file and its associates when you update RSpec. If this file is updated, you’ll lose your changes. To fix this problem, use RSpec.configure in another file: spec/support/devise.rb. RSpec automatically loads files in this directory. Let’s create this file now and fill it with the content from the following listing.
RSpec.configure do |config| config.include Devise::TestHelpers end
You should include this module only for controller tests by passing a filter to the end:
config.include Devise::TestHelpers, :type => :controller
If you don’t restrict where this module is included, it could lead to problems further down the line. It’s better to be safe than sorry.
You can specify :type => :model as a filter if you want to include a module only in your model specs. If you ever write any view specs, you can use :type => :view to include this module only in the view specs. Similarly, you can use :controller for controller specs.
Going back to your spec, you make a request on the third line to the new action in the controller. The before_filter that you haven’t yet implemented should catch the request before it gets to the action; it won’t execute the request but instead redirects the user to root_path and shows a flash[:alert] saying the user “must be an admin to do that.”
If you run this spec with bin/rspec spec/controllers/projects_controller_spec.rb, it fails as you expect:
Failure/Error: response.should redirect_to(root_path) Expected response to be a <:redirect>, but was <200>
This error message tells you that although you expected to be redirected, the response was actually a 200 response, indicating a successful response. This isn’t what you want. Now let’s get it to pass.
The first step is to define a new method that checks whether a user is an admin, and if not, displays the “You must be an admin to do that” message and then redirects the requester to the root_path. First define this new method inside app/controllers/application_controller.rb, which is the base class for all controllers and makes any methods defined here available to all controllers. Define the method using the following listing inside the ApplicationController class.
private def authorize_admin! authenticate_user! unless current_user.admin? flash[:alert] = "You must be an admin to do that." redirect_to root_path end end
This method uses the authenticate_user! method (provided by Devise) to ensure that the user is signed in. If the user isn’t signed in when this method is called, they’re asked to sign in. If the user isn’t an admin after signing in, they’re shown the “You must be an admin to do that” message and redirected to the homepage.
To call this method, call before_filter at the top of your ProjectsController, as shown in the following listing.
before_filter :authorize_admin!, :except => [:index, :show]
With that in place, you can rerun the spec bin/rspec spec/controllers/projects_controller_spec.rb, which should now pass:
2 examples, 0 failures
Great, now you know this is working for the new action, but does it work for create, edit, update, and destroy? You can replace the "cannot access the new action" example you just wrote with the code from the following listing.
{ "new" => "get", "create" => "post", "edit" => "get", "update" => "put", "destroy" => "delete" }.each do |action, method| it "cannot access the #{action} action" do sign_in(:user, user) send(method, action.dup, :id => project.id) response.should redirect_to(root_path) flash[:alert].should eql("You must be an admin to do that.") end end
In this example, you use a project variable, which you need to set up by using a let, as you did for user. Under the let for user, add one for project:
let(:project) { Factory(:project) }
The attributes of this project object are unimportant: you only need a valid object, and Factory Girl provides that for you.
The keys for the hash on the first line of listing 7.9 contain all the actions you want to ensure are protected; the values are the methods you use to make the request to the action. You use the action here to give your examples dynamic names, and you use them further down when you use the send method. The send method allows you to dynamically call methods and pass arguments to them. It’s used here because for each key-value pair of the hash, the action[1] and method change. You pass in the :id parameter because, without it, the controller can’t route to the edit, update, or destroy action. The new and create actions ignore this parameter.
1 The action variable is a frozen string in Ruby 1.9.2 (because it’s a block parameter), so you need to duplicate the object because Rails forces the encoding on it to be UTF-8.
The remainder of this spec is unchanged, and when you run bin/rspec spec/controllers/projects_controller_spec.rb, you should see all six examples passing:
6 examples, 0 failures
Now’s a good time to ensure you haven’t broken anything, so let’s run rake cucumber :ok spec:
cucumber features/deleting_projects.feature:6 cucumber features/editing_projects.feature:12 cucumber features/editing_projects.feature:18
Oops. Three scenarios are broken. They failed because, for these features, you’re not signing in as an admin user—or, in fact, as any user!—which is now required for performing the actions in the scenario. You can fix these scenarios by signing in as an admin user.
For the features/deleting_projects.feature, add a new Background, as shown in the following listing.
Background: Given there are the following users: | email | password | admin | | [email protected] | password | true | And I am signed in as them
When you run this feature, it once again passes:
1 scenario (1 passed) 8 steps (8 passed)
For the editing_projects.feature, use the steps from listing 7.10 again, putting them at the top of the already existing Background:
Given there are the following users: | email | password | admin | | [email protected] | password | true | And I am signed in as them
Now this feature also passes. Check it using bin/cucumber features/editing_projects.feature:
2 scenarios (2 passed) 21 steps (21 passed)
That should be the last of it. When you run rake cucumber:ok, everything once again passes:
16 scenarios (16 passed) 158 steps (158 passed)
Great! Now that accessing the actions is restricted, let’s make a commit here:
git add . git commit -m "Restrict access to project actions to admins only" git push
You should also hide the links from the users who are not admins, because it’s useless to show actions to people who can’t perform them.
Next you’ll learn how to hide certain links, such as the New Project link, from users who have no authorization to perform that action in your application. To begin, write a new feature called features/hidden_links.feature, which looks like the following listing.
Feature: Hidden Links In order to clean up the user experience As the system I want to hide links from users who can't act on them Background: Given there are the following users: | email | password | admin | | [email protected] | password | false | | [email protected] | password | true | And there is a project called "TextMate 2" Scenario: New project link is hidden for non-signed-in users Given I am on the homepage Then I should not see the "New Project" link Scenario: New project link is hidden for signed-in users Given I am signed in as "[email protected]" Then I should not see the "New Project" link Scenario: New project link is shown to admins Given I am signed in as "[email protected]" Then I should see the "New Project" link
When you run this feature using bin/cucumber features/hidden_links.feature, you’re given three new steps to define:
Then /^I should not see the "([^"]*)" link$/ do |arg1| pending # express the regexp above with the code you wish you had end Given /^I am signed in as "([^"]*)"$/ do |arg1| pending # express the regexp above with the code you wish you had end Then /^I should see the "([^"]*)" link$/ do |arg1| pending # express the regexp above with the code you wish you had end
Put the first and last steps in a new file called features/step_definitions/link_steps.rb, using the code from the next listing.
Then /^I should see the "([^"]*)" link$/ do |text| page.should(have_css("a", :text => text), "Expected to see the #{text.inspect} link, but did not.") end Then /^I should not see the "([^"]*)" link$/ do |text| page.should_not(have_css("a", :text => text), "Expected to not see the #{text.inspect} link, but did.") end
These two steps use the have_css method provided by Capybara, which checks that a page has an element matching a Cascading Style Sheets (CSS) matcher, in this case an element called a. The option you pass after it (:text => text) tells Capybara that you’re checking for an a element that contains the text specified. If this matcher fails, it outputs a custom error message that is the optional second argument to the should and should_not method calls here, with have_css being the first argument.
With these steps defined, you can now add the other new step to features/step_definitions/user_steps.rb using the code from the following listing.
Given /^I am signed in as "([^"]*)"$/ do |email| @user = User.find_by_email!(email) steps("Given I am signed in as them") end
This step finds the user mentioned and then calls the "Given I am signed in as them" step. Providing you always set up your users with “password” as their password, this new step will pass.
When you run your feature using bin/cucumber features/hidden_links.feature, the first two scenarios are failing:
Failing Scenarios: cucumber features/hidden_links.feature:13 cucumber features/hidden_links.feature:17
They fail, of course, because you’ve done nothing yet to hide the link! Open app/views/projects/index.html.erb, and change the New Project link to the following:
<%= admins_only do %> <%= link_to "New Project", new_project_path %> <% end %>
You’ll define the admins_only method soon, and it’ll take a block. Inside this block, you specify all the content you want shown if the user is an admin. No content will be shown if the user is not an admin. To define the admins_only helper, open app/helpers/application_helper.rb and define the method inside the module using this code:
def admins_only(&block) block.call if current_user.try(:admin?) nil end
The admins_only method takes a block, which is the code between the do and end in your view. To run this code inside the method, call block.call, which runs the specified block but only if current_user.try(:admin?) returns a value that evaluates to true. This try method tries a method on an object, and if that method doesn’t exist (as it wouldn’t if current_user were nil), then it returns nil. At the end of the method, you return nil so the content doesn’t show again.
When you run this feature using bin/cucumber features/hidden_links.feature, it passes:
3 scenarios (3 passed) 12 steps (12 passed)
Now that you’ve got the New Project link hiding if the user isn’t an admin, let’s do the same thing for the Edit Project and Delete Project links.
Add this admins_only helper to the Edit Project and Delete Project links on the projects show view, but not before adding further scenarios to cover these links to features/hidden_links.feature, as shown in the following listing.
Scenario: Edit project link is hidden for non-signed-in users Given I am on the homepage When I follow "TextMate 2" Then I should not see the "Edit Project" link Scenario: Edit project link is hidden for signed-in users Given I am signed in as "[email protected]" When I follow "TextMate 2" Then I should not see the "Edit Project" link Scenario: Edit project link is shown to admins Given I am signed in as "[email protected]" When I follow "TextMate 2" Then I should see the "Edit Project" link Scenario: Delete project link is hidden for non-signed-in users Given I am on the homepage When I follow "TextMate 2" Then I should not see the "Delete Project" link Scenario: Delete project link is hidden for signed-in users Given I am signed in as "[email protected]" When I follow "TextMate 2" Then I should not see the "Delete Project" link Scenario: Delete project link is shown to admins Given I am signed in as "[email protected]" When I follow "TextMate 2" Then I should see the "Delete Project" link
To make these steps pass, change the ProjectsController’s show template to wrap these links in the admins_only helper, as shown in the next listing.
<%= admins_only do %> <%= link_to "Edit Project", edit_project_path(@project) %> <%= link_to "Delete Project", project_path(@project), :method => :delete, :confirm => "Are you sure you want to delete this project?" %> <% end %>
When you run this entire feature using bin/cucumber features/hidden_links.feature, all the steps should pass:
9 scenarios (9 passed) 42 steps (42 passed)
All right, that was a little too easy! But that’s Rails.
This is a great point to ensure that everything is still working by running rake cucumber:ok spec. According to the following output, it is:
25 scenarios (25 passed) 200 steps (200 passed) # and 11 examples, 0 failures, 5 pending
Let’s commit and push that:
git add . git commit - m "Lock down specific projects controller actions for admins only" git push
In this section, you ensured that only users with the admin attribute set to true can get to specific actions in your ProjectsController as an example of basic authorization.
Next, you learn to “section off” part of your site using a similar methodology and explore the concept of namespacing.
Although it’s fine and dandy to ensure that admin users can get to special places in your application, you haven’t yet added the functionality for triggering whether or not a user is an admin from within the application itself. To do so, you create a new namespaced section of your site called admin. The purpose of namespacing in this case is to separate a controller from the main area of the site so you can ensure that users accessing this particular controller (and any future controllers you create in this namespace) have the admin field set to true.
You begin by generating a namespaced controller with an empty index action by using this command:
rails g controller admin/users index
When the / separator is used between parts of the controller, Rails knows to generate a namespaced controller called Admin::UsersController at app/controllers/admin/users_controller.rb. The views for this controller are at app/views/admin/users, and the spec is at spec/controllers/admin/users_controller_spec.rb.
This command also inserts a new route into your config/routes.rb file. You don’t want that, so remove this line:
get "users/index"
Now you must write a spec for this newly generated controller to ensure only users with the admin attribute set to true can access it. Open spec/controllers/admin/users_controller_spec.rb and write an example to ensure non-signed-in users can’t access the index action, as shown in the following listing.
require 'spec_helper' describe Admin::UsersController do let(:user) do user = Factory(:user) user.confirm! user end context "standard users" do before do sign_in(:user, user) end it "are not able to access the index action" do get 'index' response.should redirect_to(root_path) flash[:alert].should eql("You must be an admin to do that.") end end end
The new RSpec method, before, takes a block of code that’s executed before every spec inside the current context or describe.
You use the lengthy let(:user) block again, which is effectively the same as what you have in spec/controllers/projects_controller_spec.rb. Rather than duplicating the code inside this block, move it into a new file in your spec/support directory. Its job is to provide methods to help you seed your test data, so call it seed_helpers.rb. In this file, create a module called SeedHelpers, which contains a create_user! method that uses the code from the let. This file is shown in the following listing.
module SeedHelpers def create_user!(attributes={}) user = Factory(:user, attributes) user.confirm! user end end RSpec.configure do |config| config.include SeedHelpers end
With this new spec/support/seed_helpers.rb file, you can now use create_user! rather than the three lines of code you’re currently using. Let’s change the let(:user) in spec/controllers/projects_controller_spec.rb to this:
let(:user) { create_user! }
Ah, much better! Let’s also change it in the new spec/controllers/admin/users_controller_spec.rb file:
let(:user) { create_user! }
When you run this spec file using bin/rspec spec/controllers/admin/users_controller_spec.rb, you see that there’s no route to the index action:
1) Admin::UsersController regular users are not able to access the index action Failure/Error: get 'index' No route matches {:controller => "admin/users"}
In fact, there’s no route to the controller at all! To define this route, open config/routes.rb and insert the following code before the final end in the file.
namespace :admin do resources :users end
This code defines similar routes to the vanilla resources but nests them under an admin/ prefix. Additionally, the routing helpers for these routes have an admin part to them: what would normally be users_path becomes admin_users_path, and new_user_path becomes new_admin_user_path.
With this namespace defined, when you run bin/rspec spec/controllers/admin/users_controller_spec.rb, you should see it fail with a different error:
Failure/Error: response.should redirect_to(root_path) Expected response to be a <:redirect>, but was <200>
This error appears because you need to implement the authorize_admin !before_filter for your namespace. To apply it to all controllers in this namespace, you create a new supercontroller whose only job (for now) is to call the before_filter. You can also put methods that are common to the admin section here.
Create a new file at app/controllers/admin/base_controller.rb, and fill it with this code:
class Admin::BaseController < ApplicationController before_filter :authorize_admin! end
This file can double as an eventual homepage for the admin namespace and as a class that the other controllers inside the admin namespace can inherit from, which you’ll see in a moment. You inherit from ApplicationController with this controller so you receive all the benefits it provides, like the authorize_admin! method and the Action Controller functionality.
Open app/controllers/admin/users_controller.rb, and change the first line of the controller from this
class Admin::UsersController < ApplicationController
to this:
class Admin::UsersController < Admin::BaseController
Because Admin::UsersController inherits from Admin::BaseController, the before_filter from Admin::BaseController now runs for every action inside Admin::UsersController, and therefore in your spec, should pass.
Run it with bin/rspec spec/controllers/admin/users_controller_spec.rb now, and you should see this:
. 1 example, 0 failures
With that done, you should ensure that everything is working as expected by running rake cucumber:ok spec:
25 scenarios (25 passed) 200 steps (200 passed) # and 14 examples, 0 failures, 7 pending
Great, everything is still green! Let’s commit that:
git add . git commit -m "Added admin namespaced users controller" git push
Now that only admins can access this namespace, you can create the CRUD actions for this controller too, as you did for the TicketsController and ProjectsController controllers. Along the way, you’ll also set up a homepage for the admin namespace.
For this new CRUD resource, you first write a feature for creating a user and put it at features/creating_users.feature, as shown in the following listing.
Feature: Creating Users In order to add new users to the system As an admin I want to be able to add them through the backend Background: Given there are the following users: | email | password | admin | | [email protected] | password | true | And I am signed in as them Given I am on the homepage When I follow "Admin" And I follow "Users" When I follow "New User" Scenario: Creating a new user And I fill in "Email" with "[email protected]" And I fill in "Password" with "password" And I press "Create User" Then I should see "User has been created." Scenario: Leaving email blank results in an error When I fill in "Email" with "" And I fill in "Password" with "password" And I press "Create User" Then I should see "User has not been created." And I should see "Email can't be blank"
When you run this feature using bin/cucumber features/creating_users.feature, the first four steps pass; but when you follow the Admin link, it fails because the link doesn’t exist yet:
When I follow "Admin" no link with title, id or text 'Admin' found (Capybara::ElementNotFound)
Of course, you need this link for the feature to pass, but you want to show it only for admins. You can use the admins_only helper you defined earlier and put the link in app/views/layouts/application.html.erb in the nav element:
<%= admins_only do %> <%= link_to "Admin", admin_root_path %><br> <% end %>
At the moment, admin_root_path doesn’t exist. To define it, open config/routes.rb and change the namespace definition from this
namespace :admin do resources :users end
to this:
namespace :admin do root :to => "base#index" resources :users end
When you rerun the feature, it fails because you don’t have an index action for the Admin::BaseController controller:
When I follow "Admin" The action 'index' could not be found for Admin::BaseController
Let’s add that now.
Open app/controllers/admin/base_controller.rb, and add the index action so the class definition looks like the following listing.
class Admin::BaseController < ApplicationController before_filter :authorize_admin! def index end end
You define the action here to show users that this controller has an index action. The next step is to create the view for the index action by creating a new file at app/views/admin/base/index.html.erb and filling it with the following content:
<%= link_to "Users", admin_users_path %> Welcome to Ticketee's Admin Lounge. Please enjoy your stay.
You needn’t wrap the link in an admins_only here because you’re inside a page that’s visible only to admins. When you run the feature, you don’t get a message saying The action 'index' could not be found even though you should. Instead, you get this:
When I follow "New User" no link with title, id or text 'New User' found
This unexpected output occurs because the Admin::UsersController inherits from Admin::BaseController, where you just defined an index method. By inheriting from this controller, Admin::UsersController also inherits its views. When you inherit from a class like this, you get the methods defined in that class too. You can override the index action from Admin::BaseController by redefining it in Admin::UsersController, as in the following listing.
class Admin::UsersController < Admin::BaseController def index @users = User.all(:order => "email") end end
Next, you rewrite the template for this action, which lives at app/views/admin/users/index.html.erb, so it contains the New User link and lists all the users gathered up by the controller, as shown in the following listing.
<%= link_to "New User", new_admin_user_path %> <ul> <% @users.each do |user| %> <li><%= link_to user.email, [:admin, user] %></li> <% end %> </ul>
In this example, when you specify a Symbol as an element in the route for the link_to, Rails uses that element as a literal part of the route generation, making it use admin_user_path rather than user_path. You saw this in chapter 5 when you used it with [:edit, @project, ticket], but it bears repeating here.
When you run bin/cucumber features/creating_users.feature again, you’re told the new action is missing:
When I follow "New User" The action 'new' could not be found for Admin::UsersController
Let’s add the new action Admin::UsersController now by using this code:
def new @user = User.new end
And let’s create the view for this action at app/views/admin/users/new.html.erb:
<h2>New User</h2> <%= render "form" %>
Using the following listing, create the form partial that’s referenced in this new view at app/views/admin/users/_form.html.erb. It must contain the email and password fields, which are the bare essentials for creating a user.
<%= form_for [:admin, @user] do |f| %> <%= f.error_messages %> <p> <%= f.label :email %> <%= f.text_field :email %> </p> <p> <%= f.label :password %> <%= f.password_field :password %> </p> <%= f.submit %> <% end %>
For this form_for, you use the array form you saw earlier with [@project, @ticket], but this time you pass in a symbol rather than a model object. Rails interprets the symbol literally, generating a route such as admin_users_path rather than users_path, which would normally be generated. You can also use this array syntax with link_to and redirect_to helpers. Any symbol passed anywhere in the array is interpreted literally.
When you run the feature once again, you’re told there’s no action called create:
And I press "Create User" The action 'create' could not be found for Admin::UsersController
Let’s create that action now by using this code:
def create @user = User.new(params[:user]) if @user.save flash[:notice] = "User has been created." redirect_to admin_users_path else flash[:alert] = "User has not been created." render :action => "new" end end
With this action implemented, both scenarios inside this feature now pass:
2 scenarios (2 passed) 21 steps (21 passed)
This is another great middle point for a commit, so let’s do so now. As usual, you should run rake cucumber:ok spec to make sure everything’s still working:
27 scenarios (27 passed) 221 steps (221 passed) # and 14 examples, 0 failures, 7 pending
git add . git commit -m "Added the ability to create users through the admin backend" git push
Although this functionality allows you to create new users through the admin backend, it doesn’t let you create admin users. That’s up next.
To create admin users, you need a check box on the form that, when clicked, sets the user’s admin field to true. But, because the admin attribute isn’t on the list of accessible attributes (attr_accessible inside app/models/user.rb), it can’t be mass-assigned as the other fields can. Therefore, you must manually set this parameter in the controller before the user is saved.
To get started, let’s add another scenario to the features/creating_users.feature using the code from the following listing.
Scenario: Creating an admin user When I fill in "Email" with "[email protected]" And I fill in "Password" with "password" And I check "Is an admin?" And I press "Create User" Then I should see "User has been created" And I should see "[email protected] (Admin)"
Now when you run bin/cucumber features/creating_users.feature, it fails on the "Is an admin?" step:
cannot check field, no checkbox with id, name, or label 'Is an admin?' found (Capybara::ElementNotFound)
You want to add this check box to the form for creating users, which you can do by adding the following code to the form_for block inside app/views/admin/users/_form.html.erb:
<p> <%= f.check_box :admin %> <%= f.label :admin, "Is an admin?" %> </p>
With this check box in place, when you run bin/cucumber features/creating_users.feature, you’re told "[email protected] (Admin)" can’t be found anywhere on the page:
expected #has_content?("[email protected] (Admin)") to return true, got false
This failure occurs because admin isn’t a mass-assignable attribute and therefore isn’t set and because the user’s admin status isn’t displayed anywhere on the page. One thing at a time. First, change the create action in Admin::UsersController to set the admin field before you attempt to save the user, as shown in the following listing.
... @user = User.new(params[:user]) @user.admin = params[:user][:admin] == "1" if @user.save ...
This code sets the admin attribute on the user, which is one of the two things you need to get this step to pass. The second problem is that only the user’s email address is displayed: no text appears to indicate they’re a user. To get this text to appear, change the line in app/views/admin/users/index.html.erb from this
<li><%= link_to user.email, [:admin, user] %></li>
to this:
<li><%= link_to user, [:admin, user] %></li>
By not calling any methods on the user object and attempting to write it out of the view, you cause Ruby to call to_s on this method. By default, this outputs something similar to the following, which isn’t human friendly:
#<User:0xb6fd6054>
You can override the to_s method on the User model to provide the string containing the email and admin status of the user by putting the following code inside the class definition in app/models/user.rb, underneath the attr_accessible line:
def to_s "#{email} (#{admin? ? "Admin" : "User"})" end
Now that the admin field is set and displayed on the page, the feature should pass when you run bin/cucumber features/creating_users.feature:
3 scenarios (3 passed) 33 steps (33 passed)
This is another great time to commit, and again, run rake cucumber:ok spec to make sure everything works:
28 scenarios (28 passed) 288 steps (288 passed) # and 14 examples, 0 failures, 7 pending
git add . git commit - m "Added the ability to create admin users through the admin backend" git push
Now you can create normal and admin users through the backend. In the future, you may need to modify an existing user’s details or delete a user, so we examine the updating and deleting parts of the CRUD next.
This section focuses on creating the updating capabilities for the Admin::UsersController. Additionally, you need some functionality on the backend to enable users to confirm their account, and you can put it on the editing page.
As usual, you start by writing a feature to cover this functionality, placing the file at features/editing_users.feature and filling it with the content from the following listing.
Feature: Editing a user In order to change a user's details As an admin I want to be able to modify them through the backend Background: Given there are the following users: | email | password | admin | | [email protected] | password | true | And I am signed in as them Given there are the following users: | email | password | | [email protected] | password | Given I am on the homepage When I follow "Admin" And I follow "Users" And I follow "[email protected]" And I follow "Edit User" Scenario: Updating a user's details When I fill in "Email" with "[email protected]" And I press "Update User" Then I should see "User has been updated." And I should see "[email protected]" And I should not see "[email protected]" Scenario: Toggling a user's admin ability When I check "Is an admin?" And I press "Update User" Then I should see "User has been updated." And I should see "[email protected] (Admin)" Scenario: Updating with an invalid email fails When I fill in "Email" with "fakefakefake" And I press "Update User" Then I should see "User has not been updated." And I should see "Email is invalid"
When you run this feature using bin/cucumber features/editing_users.feature, you discover the show action is missing:
And I follow "[email protected]" The action 'show' could not be found for Admin::UsersController
Define the show action in the Admin::UsersController, shown in listing 7.28, directly under the index action, because grouping the different parts of CRUD is conventional. The method you define is blank because you need to use a before_filter to find the user, as you’ve done in other controllers to find other resources.
def show end
You call the method to find the user object find_user and define it under the actions in this controller, like this:
private def find_user @user = User.find(params[:id]) end
You then need to call this method using a before_filter, which should run before the show, edit, update, and destroy actions. Put this line at the top of your class definition for Admin::UsersController:
before_filter :find_user, :only => [:show, :edit, :update, :destroy]
With this method in place, you can write the template for the show action to make this step pass. This file goes at app/views/admin/users/show.html.erb and uses the following code:
<h2><%= @user %></h2> <%= link_to "Edit User", edit_admin_user_path(@user) %>
Now when you run bin/cucumber features/editing_users.feature, the step that previously failed passes, and you’re on to the next step:
And I follow "[email protected]" And I follow "Edit User" The action 'edit' could not be found for Admin::UsersController
Good, you’re progressing nicely. You created the show action for the Admin::UsersController, which displays information for a user to a signed-in admin user. Now you need to create the edit action so admin users can edit a user’s details.
Add the edit action directly underneath the create action in your controller. It should be another blank method like the show action:
def edit end
With this action defined and the @user variable used in its view already set by the before_filter, you now create the template for this action at app/views/admin/users/edit.html.erb. This template renders the same form as the new template:
<%= render "form" %>
When you run bin/cucumber features/editing_users.feature, you’re told the update action doesn’t exist:
The action 'update' could not be found for Admin::UsersController
Indeed, it doesn’t, so let’s create it! Add the update action to your Admin::UsersController, as shown in the following listing. You needn’t set up the @user variable here because the find_user before_filter does it for you.
def update if @user.update_attributes(params[:user]) flash[:notice] = "User has been updated." redirect_to admin_users_path else flash[:alert] = "User has not been updated." render :action => "edit" end end
With this action in place, you need to delete the password parameters from params[:user] if they are blank. Otherwise, the application will attempt to update a user with a blank password, and Devise won’t allow that. Above update_attributes, insert this code:
if params[:user][:password].blank? params[:user].delete(:password) end
Now the entire action looks like the following listing.
def update if params[:user][:password].blank? params[:user].delete(:password) params[:user].delete(:password_confirmation) end if @user.update_attributes(params[:user]) flash[:notice] = "User has been updated." redirect_to admin_users_path else flash[:alert] = "User has not been updated." render :action => "edit" end end
When you run bin/cucumber features/editing_users.feature again, the first and third scenarios pass, but the second one fails because you haven’t set the user’s admin capabilities inside the update action, as you did in the create action. To do so, remove the following line from the create action:
@user.admin = params[:user][:admin] == "1"
Now define a method called set_admin, which you can use in both actions. This method goes directly underneath find_user under the private keyword, as shown in the following listing.
private def set_admin @user.admin = params[:user][:admin] == "1" end
To use this method in the update action, place it directly above the call to update_attributes:
set_admin if @user.update_attributes(params[:user])
Placing set_admin above update_attributes ensures that the user is made an admin directly before the save for update_attributes is triggered. You should also put it before the save in the create action:
set_admin if @user.save(params[:user])
Now when you run the feature, all the scenarios pass:
3 scenarios (3 passed) 41 steps (41 passed)
In this section, you added two more actions to your Admin::UsersController: edit and update. Admin users can now update users’ details if they please.
Run rake cucumber:ok spec to ensure nothing was broken by your latest changes. You should see this output:
31 scenarios (31 passed) 270 steps (270 passed) # and 14 examples, 0 failures, 7 pending
Let’s make a commit for this new feature:
git add . git commit -m "Added ability to edit and update users" git push
With the updating done, there’s only one more part to go for your admin CRUD interface: deleting users.
There comes a time in an application’s life when you need to delete users. Maybe they asked for their account to be removed. Maybe they were being pesky. Or maybe you have another reason to delete them. Whatever the case, having the functionality to delete users is helpful.
Keeping with the theme so far, you first write a feature for deleting users (using the following listing) and put it at features/deleting_users.feature.
Feature: Deleting users In order to remove users As an admin I want to click a button and delete them Background: Given there are the following users: | email | password | admin | | [email protected] | password | true | | [email protected] | password | false | And I am signed in as "[email protected]" Given I am on the homepage When I follow "Admin" And I follow "Users" Scenario: Deleting a user And I follow "[email protected]" When I follow "Delete User" Then I should see "User has been deleted"
When you run this feature, you get right up to the first step with no issue and then it complains:
no link with title, id or text 'Delete' found (Capybara::ElementNotFound)
Of course, you need the Delete link! Add it to the show template at app/views/admin/users/show.html.erb, right underneath the Edit User link:
<%= link_to "Delete User", admin_user_path(@user), :method => :delete, :confirm => "Are you sure you want to delete this user?" %>
You need to add the destroy action next, directly under the update action in Admin::UsersController, as shown in the following listing.
def destroy @user.destroy flash[:notice] = "User has been deleted." redirect_to admin_users_path end
When you run bin/cucumber features/deleting_users.feature, the feature passes because you now have the Delete User link and its matching destroy action:
1 scenario (1 passed) 8 steps (8 passed)
There’s one small problem with this feature, though: it doesn’t stop you from deleting yourself!
To make it impossible to delete yourself, you must add another scenario to the deleting_users.feature, shown in the following listing.
Scenario: Userscannot delete themselves When I follow "[email protected]" And I follow "Delete User" Then I should see "You cannot delete yourself!"
When you run this feature with bin/cucumber features/deleting_users.feature, the first two steps of this scenario pass, but the third one fails, as you might expect, because you haven’t added the message! Change the destroy action in the Admin::UsersController to the following listing.
def destroy if @user == current_user flash[:alert] = "You cannot delete yourself!" else @user.destroy flash[:notice] = "User has been deleted." end redirect_to admin_users_path end
Now, before the destroy method does anything, it checks to see if the user attempting to be deleted is the current user and stops it with the "You cannot delete yourself!" message. When you run bin/cucumber features/deleting_users.feature this time, the scenario passes:
2 scenarios (2 passed) 16 steps (16 passed)
Great! With the ability to delete users implemented, you’ve completed the CRUD for Admin::UsersController and for the users resource entirely. Now make sure you haven’t broken anything by running rake cucumber:ok spec. You should see this output:
33 scenarios (33 passed) 286 steps (286 passed) # and 14 examples, 0 failures, 7 pending
Fantastic! Commit and push that:
git add . git commit -m "Added feature for deleting users, including protection against self-deletion"
With this final commit, you’ve got your admin section created, and it provides a great CRUD interface for users in this system so that admins can modify their details when necessary.
For this chapter, you dove into basic access control and added a field called admin to the users table. You used admin to allow and restrict access to a namespaced controller.
Then you wrote the CRUD interface for the users resource underneath the admin namespace. This interface is used in the next chapter to expand on the authorization that you’ve implemented so far: restricting users, whether admin users or not, to certain actions on certain projects. You rounded out the chapter by not allowing users to delete themselves.
The next chapter focuses on enhancing the basic permission system you’ve implemented so far, introducing a gem called cancan. With this permission system, you’ll have much more fine-grained control over what users of your application can and can’t do to projects and tickets.
3.145.101.81