Iteration J3: Limiting Access

We want to prevent people without an administrative login from accessing our site’s admin pages. It turns out that we can do it with very little code using the Rails callback facility.

Rails callbacks allow you to intercept calls to action methods, adding your own processing before they’re invoked, after they return, or both. In our case, we’ll use a before action callback to intercept all calls to the actions in our admin controller. The interceptor can check session[:user_id]. If it’s set and if it corresponds to a user in the database, the application knows an administrator is logged in, and the call can proceed. If it’s not set, the interceptor can issue a redirect, in this case to our login page.

Where should we put this method? It could sit directly in the admin controller, but—for reasons that’ll become apparent shortly—let’s put it instead in ApplicationController, the parent class of all our controllers. This is in the application_controller.rb file in the app/controllers directory. Note too that we chose to restrict access to this method. This prevents it from ever being exposed to end users as an action:

rails51/depot_r/app/controllers/application_controller.rb
 class​ ApplicationController < ActionController::Base
» before_action ​:authorize
 
 # ...
»
»protected
»
»def​ authorize
»unless​ User.find_by(​id: ​session[​:user_id​])
» redirect_to login_url, ​notice: ​​"Please log in"
»end
»end
 end

The before_action line causes the authorize method to be invoked before every action in our application.

This is going too far. We’ve just limited access to the store itself to administrators. That’s not good.

We could go back and change things so that we mark only those methods that specifically need authorization. Such an approach, called blacklisting, is prone to errors of omission. A much better approach is to whitelist—list methods or controllers for which authorization is not required. We do this by inserting a skip_before_action call within the StoreController:

rails51/depot_r/app/controllers/store_controller.rb
 class​ StoreController < ApplicationController
» skip_before_action ​:authorize

And we do it again for the SessionsController class:

rails51/depot_r/app/controllers/sessions_controller.rb
 class​ SessionsController < ApplicationController
» skip_before_action ​:authorize

We’re not done yet; we need to allow people to create, update, and delete carts:

rails51/depot_r/app/controllers/carts_controller.rb
 class​ CartsController < ApplicationController
» skip_before_action ​:authorize​, ​only: ​[​:create​, ​:update​, ​:destroy​]

And we allow them to create line items:

rails51/depot_r/app/controllers/line_items_controller.rb
 class​ LineItemsController < ApplicationController
» skip_before_action ​:authorize​, ​only: :create

We also allow them to create orders (which includes access to the new form):

rails51/depot_r/app/controllers/orders_controller.rb
 class​ OrdersController < ApplicationController
» skip_before_action ​:authorize​, ​only: ​[​:new​, ​:create​]

With the authorization logic in place, we can now navigate to http://localhost:3000/products. The callback method intercepts us on the way to the product listing and shows us the login screen instead.

Unfortunately, this change pretty much invalidates most of our functional tests, because most operations will now redirect to the login screen instead of doing the function desired. Fortunately, we can address this globally by creating a setup method in the test_helper. While we’re there, we also define some helper methods to login_as and logout a user:

rails51/depot_r/test/test_helper.rb
 class​ ActionDispatch::IntegrationTest
 def​ login_as(user)
  post login_url, ​params: ​{ ​name: ​user.name, ​password: ​​'secret'​ }
 end
 
 def​ logout
  delete logout_url
 end
 
 def​ setup
  login_as users(​:one​)
 end
 end

Note that the setup method will call login_as only if session is defined. This prevents the login from being executed in tests that don’t involve a controller.

We show our customer and are rewarded with a big smile and a request: could we add a sidebar and put links to the user and product administration stuff in it? And while we’re there, could we add the ability to list and delete administrative users? You betcha!