Rails advanced routing constraints

Ask questions Research chat →

https://thoughtbot.com/blog/rails-advanced-routing-constraints · scraped

rails

Attachments

Scraped Content

— 524 words · 2026-02-14 03:21:13 UTC ·

Excerpt

I’m on a client project that’s using Devise. In an effort to prevent anonymous users from accessing admin routes, we wrap those routes with an authenticated constraint. This constraint also ensures only authenticated users who are admins are allowed access. # config/routes.rb authenticated :user, -> { _1.admin? } do namespace :admin do resources :users end end We recently needed to restrict access to the admin routes based on IP address. Our first approach was to place this logic at the controller layer and use a before_action filter. # app/controllers/admin/users_controller.rb class Admin::UsersController < ApplicationController before_action :authorize_ip private def authorize_ip allow_list = Rails.application.config.x.ips.allow_list raise ActionController::RoutingError.new("Not Found") unless requests.ip.in? allow_list end end However, we realized there was an opportunity to push this logic to the routing layer, since we already have access to the request o
I’m on a client project that’s using Devise. In an effort to prevent anonymous users from accessing admin routes, we wrap those routes with an authenticated constraint. This constraint also ensures only authenticated users who are admins are allowed access. # config/routes.rb authenticated :user, -> { _1.admin? } do namespace :admin do resources :users end end We recently needed to restrict access to the admin routes based on IP address. Our first approach was to place this logic at the controller layer and use a before_action filter. # app/controllers/admin/users_controller.rb class Admin::UsersController < ApplicationController before_action :authorize_ip private def authorize_ip allow_list = Rails.application.config.x.ips.allow_list raise ActionController::RoutingError.new("Not Found") unless requests.ip.in? allow_list end end However, we realized there was an opportunity to push this logic to the routing layer, since we already have access to the request object. This saves us from having to process the request in a controller altogether, which is a small performance gain. Although Rails provides the ability to restrict routes based on IP range, we needed to create a custom constraint in order to see if the IP was in our allow list, which is not possible otherwise. The custom constraint needs to respond to matches? when passed a request, and must return a boolean. To see if a request is from an IP address in our allow list, we can do something like this: # app/constraints/ip_constraint.rb class IpConstraint def self.matches?(request) allow_list = Rails.application.config.x.ips.allow_list request.ip.in? allow_list end end Then we can wrap our admin routes in this constraint like so. # config/routes.rb authenticated :user, -> { _1.admin? } do constraints(IpConstraint) do namespace :admin do resources :users end end end Although the previous implementation is perfectly acceptable, there’s an opportunity to consolidate the authenticated constraint with our IpConstraint. Since we need access to the user, we can leverage warden (which is a dependency of Devise) to return the user object from the request. requst.env["warden"].user # => #<User> We can then combine this with the logic used to check the IP address like so: # app/constraints/admin_constraint.rb class AdminConstraint attr_reader :user, :ip def initialize(request) @user = request.env["warden"].user @ip = request.ip end def self.matches?(request) new(request).authorized? end def authorized? allow_list = Rails.application.config.x.ips.allow_list ip.in?(allow_list) && user.present? && user.admin? end end A constraint needs to respond to matches?, so we are free to put whatever logic we want in that method so long as it returns boolean. In this case, our matches? method initializes a new instance of our constraint and calls authorized?. The authorized? method is responsible for determining if the request came from a supported IP address, and that the requested came from an authenticated admin. Now we can update our routes like so: # config/routes.rb constraints(AdminConstraint) do namespace :admin do resources :users end end I think this is an appropriate strategy for authorizing requests at the routing layer (instead of the controller layer) because it is only concerned with data in the request. If you need data beyond the raw request, then you should leverage authorization libraries such as Pundit. Learn about the Ruby on Rails services thoughtbot offers and how we can work together to streamline your project. If you enjoyed this post, you might also like:

Visibility

Visible to everyone

Reading Status

Related Bookmarks

My Note


Saved!

Annotations

Export as Markdown
+ Annotate selection

Add Annotation