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:
| 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:
| class StoreController < ApplicationController |
» | skip_before_action :authorize |
And we do it again for the SessionsController class:
| class SessionsController < ApplicationController |
» | skip_before_action :authorize |
We’re not done yet; we need to allow people to create, update, and delete carts:
| class CartsController < ApplicationController |
» | skip_before_action :authorize, only: [:create, :update, :destroy] |
And we allow them to create line items:
| class LineItemsController < ApplicationController |
» | skip_before_action :authorize, only: :create |
We also allow them to create orders (which includes access to the new form):
| 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:
| 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!