Rails restricted paths validator for URL slugs

When we move from model IDs to slugs, we have to pay attention to controller actions’ names collision.

A while back, I wrote about the simplest method to implement slugs in Rails. While the implementation is easy, it’s not everything we have to pay attention to. Once we move from IDs to slugs, we need to make sure our users don’t choose slugs that would break the application.

The /teams/:slug path could override or be overriden by /teams/controller_action_name.

The following snipped would be a good start as alphanumeric restrictions are good for a URL identifier:

class Team < ApplicationRecord
  ...

  validates :slug,
    presence: true,
    uniqueness: true,
    length: {minimum: 2, maximum: 30},
    format: {with: /\A[a-zA-Z0-9]+\Z/}

But it’s not enough. The entered slugs could still collide with regular application paths. What if someone choose the name of your controller action? Or something you want to reserve for the future?

To combat this, we could write a custom validator for this purpose:

# app/validators/restricted_paths_validator.rb

class RestrictedPathsValidator < ActiveModel::Validator
  RESTRICTED_PATHS = TeamsController.action_methods + [
    "admin",
    "admins"
  ]

  def validate(record)
    if RESTRICTED_PATHS.include?(record.slug)
      record.errors.add :slug, :restricted_path
    end
  end
end

Then include it in your model:

  # Avoid slug conflicts with routes
  validates_with ::RestrictedPathsValidator

The validator is restricting all current actions from the controller and more paths can be added on top.

If we want to provide a custom error message, we can add an I18n entry for activerecord.errors.messages.restricted_path:

en:
  activerecord:
    errors:
      messages:
        restricted_path: not allowed

← IT'S OUT NOW

I wrote a complete guide on web application deployment. Ruby with Puma, Python with Gunicorn, NGINX, PostgreSQL, Redis, networking, processes, systemd, backups, and all your usual suspects.

More →