Chapter 7. Basic access control

This chapter covers

  • Adding an authorization flag to a database table
  • Locking down access based on a database flag

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.

7.1. Projects can be created only by admins

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.

Listing 7.1. features/creating_projects.feature
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.

Listing 7.2. features/creating_projects.feature
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: ...>

7.2. Adding the admin field to the users table

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.

7.3. Restricting actions to admins only

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.

Listing 7.3. spec/controllers/projects_controller_spec.rb
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.

Listing 7.4. spec/controllers/projects_controller_spec.rb
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.

Listing 7.5. spec/spec_helper.rb
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.

Listing 7.6. spec/support/devise.rb
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.

 

Tip

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.

Listing 7.7. app/controllers/application_controller.rb
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.

Listing 7.8. app/controllers/projects_controller.rb
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.

Listing 7.9. spec/controllers/projects_controller_spec.rb
{ "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.

7.3.1. Fixing three more broken scenarios

For the features/deleting_projects.feature, add a new Background, as shown in the following listing.

Listing 7.10. features/deleting_projects.feature
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.

7.3.2. Hiding the New Project link

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.

Listing 7.11. features/hidden_links.feature
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.

Listing 7.12. features/step_definitions/link_steps.rb
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.

Listing 7.13. features/step_definitions/user_steps.rb
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.

7.3.3. Hiding the edit and delete 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.

Listing 7.14. features/hidden_links.feature
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.

Listing 7.15. app/views/projects/show.html.erb
<%= 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.

7.4. Namespace routing

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.

Listing 7.16. spec/controllers/admin/users_controller_spec.rb
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.

Listing 7.17. spec/support/seed_helpers.rb
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.

Listing 7.18. config/routes.rb
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

7.5. Namespace-based CRUD

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.

Listing 7.19. features/creating_users.feature
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)

7.5.1. Adding a namespace root

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.

7.5.2. The index action

Open app/controllers/admin/base_controller.rb, and add the index action so the class definition looks like the following listing.

Listing 7.20. app/controllers/admin/base_controller.rb
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.

Listing 7.21. app/controllers/admin/users_controller.rb
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.

Listing 7.22. app/views/admin/users/index.html.erb
<%= 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

7.5.3. The new action

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.

Listing 7.23. app/views/admin/users/_form.html.erb
<%= 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

7.5.4. The create action

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

Great! Let’s push that:

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.

7.6. Creating admin users

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.

Listing 7.24. features/creating_users.feature
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.

Listing 7.25. app/controllers/admin/users_controller.rb
...
@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

Good stuff. Push it:

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.

7.7. Editing users

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.

Listing 7.26. features/editing_users.feature
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

7.7.1. The show action

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.

Listing 7.27. app/controllers/admin/users_controller.rb
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.

7.7.2. The edit and update actions

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.

Listing 7.28. app/controllers/admin/users_controller.rb
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.

Listing 7.29. app/controllers/admin/users_controller.rb
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.

Listing 7.30. app/controllers/admin/users_controller.rb
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.

7.8. 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.

Listing 7.31. 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.

Listing 7.32. app/controllers/admin/users_controller.rb
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!

7.8.1. Ensuring you can’t delete yourself

To make it impossible to delete yourself, you must add another scenario to the deleting_users.feature, shown in the following listing.

Listing 7.33. features/deleting_users.feature
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.

Listing 7.34. app/controllers/admin/users_controller.rb
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.

7.9. Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.101.81