We have updated the content of our program. To access the current Software Engineering curriculum visit curriculum.turing.edu.
Authorization in Rails
Learning Goals
- Authorize users based on roles
- Write a feature test that fakes a user being logged in
- Implement namespacing for routes
- Use a
before_action
to protect admin controllers
Slides
Warmup
- What does it mean to authorize something?
- What do you think the difference between authentication and authorization is?
- Any thoughts on how we might use namespacing to help us organize our authorization strategy?
Repo
We will continue working in Set List, so pull down the most recent version of that here.
Code Along
Adding Authorization to our App
Let’s create a test for the admin functionality we want to create.
Our client has asked for admin of the app to have the ability to email users. Only admin should be able to access this functionality.
Let’s write our test.
# spec/features/admin/email_spec.rb
require "rails_helper"
describe "Admin can email users" do
describe "As an admin" do
it "I can see a link on my dashboard to email a user" do
admin = User.create(username: "penelope",
password: "boom",
role: 1)
allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(admin)
visit "/admin/dashboard"
expect(page).to have_link("Email a User")
end
end
end
Wait, what’s going on with that role
field?
Basically, we’ll need that in order to determine what level of authorization a user has within the scope of our application. This is a distinction that we’re going to want to be able to make in our application.
Let’s drop down into a model test to see if we can develop this behavior.
# spec/models/user_spec.rb
require 'rails_helper'
describe User do
describe "roles" do
it "can be created as an admin" do
user = User.create(username: "penelope",
password: "boom",
role: 1)
expect(user.role).to eq("admin")
expect(user.admin?).to be_truthy
end
it "can be created as a default user" do
user = User.create(username: "sammy",
password: "pass",
role: 0)
expect(user.role).to eq("default")
expect(user.default?).to be_truthy
end
end
end
Our error should read something like this:
An error occurred while loading ./spec/features/admin/email_spec.rb.
Failure/Error: admin = User.create(username: "penelope", password: "boom", role: 1)
ActiveModel::UnknownAttributeError:
unknown attribute 'role' for User.
This might seem a little bit crazy at first. The tests only have two lines, and those two lines don’t even seem to go together very well. We create a User
with a role
of 1 and then proceed to assert that the user’s role is "admin"
. And that last line feels a little redundant? What’s happening?
The behavior that we’re describing is going to be achieved using an enum
. For more information check out the docs. To start, let’s add a migration that will create role
on our User
.
$ rails g migration AddRoleToUsers role:integer
That should give us a starting point. Edit the new migration to include a default value of 0.
class AddRoleToUsers < ActiveRecord::Migration[5.1]
def change
add_column :users, :role, :integer, default: 0
end
end
Don’t forget to run rake db:migrate
to run this migration! Check your db/schema.rb
to ensure that our schema reflects these changes.
We’re halfway there! We’ve added a role, but we’ll still get a fairly useless error.
Failures:
1) User roles can be created as an admin
Failure/Error: expect(user.role).to eq("admin")
expected: "admin"
got: 1
(compared using ==)
# ./spec/models/user_spec.rb:25:in `block (3 levels) in <top (required)>'
2) User roles can be created as a default user
Failure/Error: expect(user.role).to eq("default")
expected: "default"
got: 0
(compared using ==)
# ./spec/models/user_spec.rb:34:in `block (3 levels) in <top (required)>'
At this point, let’s add our enum
to our User
model.
class User < ApplicationRecord
has_secure_password
validates :username, presence: true,
uniqueness: true
enum role: %w(default admin)
end
Let’s check, and… a passing model test! With that out of the way, let’s go back and tackle our the error that our integration test is throwing our way.
Failure/Error: visit "/admin/dashboard"
ActionController::RoutingError:
No route matches [GET] "/admin/dashboard"
We want admin functionality when it comes to dashboard
. Only admins should be able to see the index view for the dashboard and anything that’s in the dashboard.
Do you think we should we use nested resources here or namespace?
Since admin
is never going to be its own entity (resource) within our application, we can use namespace
.
Add this namespace
and the route for only the index
to your routes (we may need to add other actions later, but for now, let’s only create what we need).
namespace :admin do
#only admin users will be able to reach this resource
get '/dashboard', to: "dashboard#index"
end
Whenever we add something to our routes.rb
, verify that your output via rails routes
is what you expect.
Whoa, our routes are getting long and hard to read!
Let’s run rails routes -c admin
to see just our admin routes.
Routes are looking good. What about our specs?
ActionController::RoutingError: uninitialized constant Admin
Remember when we want to use namespace, its controllers need to be places within a folder that shares a name with the namespace.
$ mkdir app/controllers/admin
Run our test, and we’ll see that we’re getting closer, but still not quite there.
Failure/Error: visit "/admin/dashboard"
ActionController::RoutingError: uninitialized constant Admin::DashboardController
The big piece of news in this error is that we don’t have an Admin::DashboardController
. Let’s go ahead and build that now.
$ touch app/controllers/admin/dashboard_controller.rb
And run our test suite to see what error we’re getting now:
Failure/Error: visit "/admin/dashboard"
ActionController::RoutingError: uninitialized constant Admin::DashboardController
Errors like this are great. They reiterate to me that I’ve created the file that I wanted to, Rails is looking in that file, and it’s just not finding what it expects. Let’s populate that file now and help it out.
class Admin::DashboardController < ApplicationController
end
And our new error:
AbstractController::ActionNotFound:
The action 'index' could not be found for Admin::DashboardController
So, let’s add an index
method to our controller.
class Admin::DashboardController < ApplicationController
def index
end
end
Running the test gives us the Missing template
error that we’d expect. Let’s add that view.
$ mkdir app/views/admin
$ mkdir app/views/admin/dashboard
$ touch app/views/admin/dashboard/index.html.erb
If we look back at our test we can start to figure out what might be happening. We need to add a link to our new page in order to make it pass.
In app/views/admin/dashboard/index.html.erb
:
<%= link_to "Email a User" %>
Let’s run our test… And it passes! Great! We’re done here, right? I mean, passing test!
Question: have we done anything up to this point that’s really new? Anything that would give us any confidence that we’ve actually authorized a user? We’ve gone through the trouble of creating a place to put some of our admin functionality, but I don’t see anywhere that we’re actually limiting access to that space. Let’s create a test to make sure a default user can’t get to all this sweet admin goodness. If we can make that pass I’ll start to feel a little bit better about our authentication.
In our email_spec.rb
file let’s add the following:
describe "as default user" do
it 'does not allow default user to see admin dashboard index' do
user = User.create(username: "fern@gully.com",
password: "password",
role: 0)
allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user)
visit "/admin/dashboard"
expect(page).to_not have_link("Email a User")
expect(page).to have_content("The page you were looking for doesn't exist.")
end
end
Run that, and sure enough we have a failing test. Our regular old users can get to everything that our admin can. Let’s change that.
Inside of our Admin::DashboardController
, let’s add a before_action
to check to see if a user is an admin before they access any of the Dashboard
functionality.
class Admin::DashboardController < ApplicationController
before_action :require_admin
def index
end
private
def require_admin
render file: "/public/404" unless current_admin?
end
end
So, at a high level this makes sense: we’ve created a before_action
to check to see if a user is a current admin, but if we run the test we now have two errors. (The second is the same as the first)
Failures:
1) Failure/Error: render file: "/public/404" unless current_admin?
NoMethodError:
undefined method `current_admin?' for #<Admin::DashboardController:0x007fabfb9dfc70>
Did you mean? current_user
We haven’t defined current_admin?
. Let’s define that the same place that we define current_user
in our ApplicationController
.
# application_controller.rb
def current_admin?
current_user && current_user.admin?
end
What gives us access to current_user.admin?
? Our enum!
Our test should be passing at this point. And we’re happy. We have some confidence that our regular old user can’t access the dashboard functionality that we’ve namespaced to admin
. How can we make this better?
Let’s refactor so that we’re not limiting this functionality to just our dashboard
controller. Then we should be able to stuff some more controllers into our namespace and give them the same level of protection.
Create a new BaseController
in our admin folder.
$ touch app/controllers/admin/base_controller.rb
Inside of base_controller.rb
, copy in the before_action
that we had previously put directly on our Admin::DashboardController
.
class Admin::BaseController < ApplicationController
before_action :require_admin
def require_admin
render file: "/public/404" unless current_admin?
end
end
Now, delete that from our Admin::DashboardController
, and instead have it inherit from our newly created Admin::BaseController
. When you’re finished, your dashboard controller should look like this:
class Admin::DashboardController < Admin::BaseController
def index
end
end
WrapUp
- What’s the difference between Authentication and Authorization?
- Why are both necessary for securing our applications?
- What’s a
before_action
filter in Rails? - What’s an
enum
attribute in ActiveRecord? Why would we ever want to use this? - When thinking about Authorization, why might we want to namespace a resource?
- What does
allow_any_instance_of
in RSpec do?
Work Time
Add admin functionality to your current group project and be sure to authorize an admin correctly.