I have a Ruby on Rails app with an admin section, as is common for a lot of apps. The admin section is namespaced, so all of the controllers are named something like Admin::WidgetsController and the routes all start with /admin.

Now, there is no reason, ever, for a non-admin user to do anything in the admin section, so I created an Admin::ApplicationController and put a before_filter in it to redirect requests from non-admin users to the admin login page. Then, I had all of my Admin:: controllers inherit from it.

So I'm all set, right?

Of course, the weak link in this chain is that I have to remember to make my Admin:: controllers inherit from Admin::ApplicationController. But I'm just a human -- and I frequently use generators to create my controllers -- so I'm always forgetting to do this.

But this is a huge sercurity problem. And, now that I know how to test for orphaned routes, testing that all of my /admin routes are secure is a piece of cake.

In fact, this test caught me forgetting to inherit from Admin::ApplicationController not more than 3 hours ago. Enjoy!


require 'test_helper'

class Admin::InsecureRoutesTest < ActionDispatch::IntegrationTest
  ROUTES = Rails.application.routes.routes.map do |route|
    # Turn the route path spec into a string:
    # - Remove the "(.:format)" bit at the end
    # - Use "1" for all params
    path = route.path.spec.to_s.gsub(/\(\.:format\)/, "").gsub(/:[a-zA-Z_]+/, "1")
    # Route verbs are stored as regular expressions; convert them to symbols
    verb = %W{ GET POST PUT PATCH DELETE }.grep(route.verb).first.downcase.to_sym
    # Return a hash with two keys: the route path and it's verb
    { path: path, verb: verb }
  end

  test "admin routes should redirect to admin login page" do
    admin_routes = ROUTES.select { |route| route[:path].starts_with? "/admin" }
    insecure_routes = []

    admin_routes.each do |route|
      begin
        reset!
        # Use the route's verb to access the route's path
        request_via_redirect(route[:verb], route[:path])
        
        # If we aren't redirected to the admin login, the route is insecure
        unless path == admin_login_path
          insecure_routes << "#{route[:verb]} #{route[:path]}"
        end

        rescue ActiveRecord::RecordNotFound
        # Since we are blindly submitting "1" for all route params,
        # this error can pop up. If it does, it means that the request
        # got past the `before_filter` and to the code in the controller
        # where it attempts to locate the record in the database. That
        # means this is an insecure route.
        unless path == admin_login_path
          insecure_routes << "#{route[:verb]} #{route[:path]}"
        end

        rescue AbstractController::ActionNotFound
        # This error means the route doesn't connect to a controller
        # action. This is a problem if it happens, but this is a separate
        # concern and should be tested elsewhere. See my previous post
        # about testing for orphaned routes.
      end
    end

    # Fail if we have insecure routes.
    assert insecure_routes.empty?, "The following routes are not secure: \n\t#{insecure_routes.uniq.join("\n\t")}"
  end