Many times we mount engines and restrict access to admin users via Devise. In this post, I’ll show you how to do the same when using a different authentication mechanism.
Let’s take for example the Sidekiq engine. According to their wiki, all we need to do is surround the mount using the authenticate method.
# config/routes.rb
authenticate :user, ->(user) { user.admin? } do
mount Sidekiq::Web => '/sidekiq'
end
But since this method is a Devise helper method, how can we achieve the same results when we use a different authentication mechanism?
Turns out it’s actually very simple. We can use a Rails’ advanced constraint.
# config/routes.rb
mount Sidekiq::Web, at: '/sidekiq', constraints: AdminConstraint
Not too shabby! It looks even better than the Devise helper method IMO. But let’s dive into this constraint.
For the sake of simplification, I will assume that our authentication mechanism consist of a JWT token which gets saved on a cookie and a service which takes care of verifying that token. This service will also return a user when successful or nil otherwise. Replace this behaviour for whatever mechanism you have instead.
# app/constraints/admin_contraint.rb
class AdminConstraint
class << self
def matches?(request)
user = TokenAuthenticationService.new(request.cookies['authToken']).call
user.present? && user.admin?
end
end
end
Yes, it’s a bit more code, but not that much and it allows us to leave the routes file a bit cleaner and to have a single place where to define what access to admin means.
Let’s finish the job by adding a test. I like RSpec, so I’ll write a request tests.
I’ll also assume that you have a token generation service.
# spec/constraints/admin_constraint_spec.rb
require "rails_helper"
# we won't want to rely on sidekiq for our test, so we'll create a dummy Engine
module MyEngine
class Engine < ::Rails::Engine
isolate_namespace MyEngine
end
class LinksController < ::ActionController::Base
def index
render plain: 'hit_engine_route'
end
end
end
MyEngine::Engine.routes.draw do
resources :links, :only => [:index]
end
module MyEngine
RSpec.describe "Links", :type => :request do
include Engine.routes.url_helpers
before do
Rails.application.routes.draw do
mount MyEngine::Engine => "/my_engine", constraints: AdminConstraint
end
cookies['authToken'] = token
end
after do
Rails.application.routes_reloader.reload!
end
let(:token) { TokenGeneratorService.new(user).call }
context 'with an admin token cookie' do
let(:user) { create(:user, admin: true) }
it "is found" do
get links_url
expect(response).to have_http_status(:ok)
expect(response.body).to eq('hit_engine_route')
end
end
context 'with a non-admin user' do
let(:user) { create(:user, admin: false) }
it "is not found" do
expect {
get links_url
}.to raise_error(ActionController::RoutingError)
end
end
end
end
Et voila! We’re sure that our constraint behaves as expected.
Resources
All the code in this post was based on the documentation from the following projects: