Mauro Morales

software developer

Tag: Ruby on Rails

  • Rails Routing Advanced Constraints for User Authentication Without Devise

    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:

  • Ruby On Rails: Storing JSON Directly in PostgreSQL

    Whenever we save data from one of our Rails models, each attribute is mapped one to one to a field in the database. These fields are generally of a simple type, like a string or an integer. However, it’s also possible to save an entire data object in JSON format in a field. Let’s see an example of how to do this from a Ruby on Rails application.

    For this example, let’s assume that I have a Page model where I want to save some stats. To begin, we’re going to generate a new migration and add the stats field and define it as type JSON which by default will save an empty array

    def change
      add_column :pages, :stats, :json, default: []
    end

    Once migrated, let’s have a deeper look at how our pages table looks like

    \d pages
    Table "public.pages"
     Column | Type | Default
    ...
     stats  | json | '[]'::json

    Now that is interesting, unlike the more common types which can be 0 or false, the default value of this field is literally the string cast to JSON. Let’s play a little bit with this and cast an array with values

    SELECT '[1, 2, 3]'::json
       json    
    -----------
     [1, 2, 3]
    (1 row)

    Turns out PostgreSQL offers also a set of functions to handle JSON data. Let’s say for example that I wanted to get all pages that have no pre-calculated stats. This can be done using the json_array_length function

    SELECT *
      FROM people
     WHERE json_array_length(stats) = 0

    This is way more performant than, fetching the data, serializing it, and loading it to a Ruby array to then calculate its length.

    Ok, that’s all nice, what about the cases when I do need to load the data in a Ruby object and then save it back? You’ll be happy to know that you don’t need to do anything else, Rails will do all the heavy lifting of serializing and deserializing for you, and provide a getter and setter methods so you can interact with the attribute as you normally would

    page = Page.find(1)
    page.stats.class
    => Array
    page.stats = [1, 2, 3]
    => [1, 2, 3]
    page.save
    => true

    Throughout this example, I used a very simple array, but you can of course use much more complex data objects like you normally would with JSON but be careful not to shoot yourself in the foot! Just because you can save a lot of data into a JSON field doesn’t mean that you should. Evaluate first if what you need is an additional model that relates to the model you’re working with.

    Want to know more? Checkout PostgreSQL documentation on the JSON datatype and the functions you can use

  • ActiveRecord Except

    August 19th was Whyday, and to commemorate it, I decided to write a gem called activerecord-except.

    WHAT IS IT?

    activerecord-except is a Ruby gem that extends the functionality of ActiveRecord, allowing you to select all the fields from a table, except the ones that you specify. For example, if you have a table users.

    development_db=# \d users
          Table "public.users"
                Column             | 
    -------------------------------+-
     id                            | 
     username                      | 
     password                      | 
     email                         | 
     first_name                    | 
     last_name                     | 
     phone                         | 
     ...
     created_at                    | 
     updated_at                    | 

    And you want to get all the fields except for the password, you’d have to pass each of them in your select clause like so

    User.all.select(:id,
                    :username,
                    :email,
                    :first_name,
                    :last_name,
                    :phone,
                    ...
                    :created_at,
                    :updated_at)

    Instead, using activerecord-except, can simplify your statement by saying only the field you don’t want, in this case, the password one

    User.all.except(:password)

    HOW DOES IT WORK?

    Under the hood, the except clause makes use of the traditional selectclause. So our previous example will produce the following query

    SELECT "users"."id",
           "users"."username",
           "users"."first_name",
           "users"."last_name",
           "users"."phone",
           "users"."created_at",
           ...
           "users"."updated_at"
      FROM "users"

    This is because the SQL language doesn’t provide such functionality out of the box.

    I don’t know what is the reason for this. I can only speculate that it’s to be more explicit and not be caught by surprise if a field in a table gets added/deleted/changed. However, * is also wildly used. In Rails for example, it is what you get, when you don’t specify a select clause in your query.

    The way I managed to make it work is by adding a method toActiveRecord::Relation which asks the model for all its attributes and rejecting those that match with the ones passed as arguments.

    klass._default_attributes
         .keys.map(&:to_sym)
         .reject { |attr| fields.include?(attr) }
    

    Note: As you can see, I’m using _default_attributes which starts with an underscore. This can mean that the method is not intended to be relied upon.

    Whether or not you might want to use in production, I leave up to you, where I really see the benefit of activerecord-except is for writing one-off scripts to extract data, because it makes them much easier to read.

    WANT TO GIVE IT A TRY?

    You can install it from rubygems or you can check the source code either on Sourcehut or GitHub.