Hot Glue Example #3 – Pet Spa

Example 3: Hawked Foreign Keys

The hawk is a bird that flies through your app. Her responsibility is to make sure that access-controlled users (that is, non-Gd controllers) can see & set foreign keys to related records only within the parameters of their access control. It is up to you to specify the hawk correctly, which this tutorial will teach you to do.

Remember, by default an access-controlled controller will give “me” access to only records I’m supposed to see. This can be achieved in different ways— but the standard way is for there to be a foreign key on the current table to the current user. Another way to do that is for the controller to be nested within a parent object where the access control is provided. (You don’t need a foreign key on the child objects if you are providing access control from the parent object through nested routes.)

But remember when you have access control on the current controller, new records automatically are associated to the current user. That’s what the --auth and --auth_identifier flags help with.

But let’s say you have another foreign key — somewhere else on the table— that should be scoped to the object chain that stems from the current user.

That’s when you use the hawk.

If you fail to use the hawk on access control controllers, except as provided by the nesting architecture, your users will have full access to all records in the database when setting foreign keys. (That is, foreign keys that aren’t part of the existing access control provided by the starfish access control using nested routes.)

Without the hawk, users will be able to set a foreign key to an object even if they don’t actually own that object.

Inflections

Setup on Inflections

Open the existing file at config/initializers/inflections.rb

Notice that everything in the file config/initializers/inflections is commented out by default.

# Be sure to restart your server when you modify this file.

# Add new inflection rules using the following format. Inflections
# are locale specific, and you may define rules for as many different
# locales as you wish. All of these examples are active by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
#   inflect.plural /^(ox)$/i, "\\1en"
#   inflect.singular /^(ox)en/i, "\\1"
#   inflect.irregular "person", "people"
#   inflect.uncountable %w( fish sheep )
# end

# These inflection rules are supported but not enabled by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
#   inflect.acronym "RESTful"
# end

Uncomment these lines:

ActiveSupport::Inflector.inflections 
   inflect.plural /^(ox)$/i, "\\1en"
   inflect.singular /^(ox)en/i, "\\1"
   inflect.irregular "person", "people"
   inflect.uncountable %w( fish sheep )
   inflect.irregular "human", "humen"
end

Add the line in orange.

Save and commit the file config/initializers/inflections before continuing.

The Setup

rails new PetSpa --database=postgresql --javascript=esbuild --css=bootstrap

MODEL GENERATORS

rails generate devise Human name:string is_admin:boolean

rails generate model Pet name:string human_id:integer

rails generate model Appointment when_at:datetime pet_id:integer

models/appointment.rb

class Appointment < ApplicationRecord
  belongs_to :pet

  has_one :human, through: :pet

  def name
    "for #{pet.try(:name)} @ #{when_at}"
  end
end

models/human.rb

class Human < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :pets, dependent: :destroy
  has_many :appointments, through: :pets
  
  before_validation :check_if_missing_password!

  def check_if_missing_password!
    if encrypted_password.nil? || encrypted_password.empty?
      new_password = (0...8).map { (65 + rand(26)).chr }.join
      self.password = new_password
      self.password_confirmation = new_password
    end
  end
end

models/pet.rb

class Pet < ApplicationRecord
  belongs_to :human

  has_many :appointments, dependent: :destroy
end

CONTROLLER

rails generate controller Welcome

class WelcomeController < ApplicationController
  before_action :authenticate_human!

  def index
    redirect_to dashboard_pets_path
  end
end

config/routes.rb

Rails.application.routes.draw do
  devise_for :human

  root "welcome#index"

  namespace :admin do
    resources :humans do
      resources :pets
    end
    resources :appointments
  end

  namespace :dashboard do
    resources :pets
    resources :appointments
  end
end

THE ADMIN VIEWS

For setup purposes, there are admin views at /admin/humans and /admin/appointments. Notice that the humans scaffold has a downnested scaffold (child portal) to the related pets. This makes it easy to see the humans & their pets together.

rails generate hot_glue:scaffold Human --namespace=admin --gd --downnest=pets --smart-layout

rails generate hot_glue:scaffold Pet --namespace=admin --gd --nested=human --smart-layout

rails generate hot_glue:scaffold Appointment --namespace=admin --gd --smart-layout

In our example, there are two humans: me (Jason) and Mary. You can recognize my pet names because I have pets with names that sound like pet names, but Mary names her pets after obscure Greek names (Cornelius, Naabhi, and Tabassum)

On the admin view, we can see both humans & their pets:

Hot Glue hawking example demonstration

Keep in mind that Jason’s pet names are Fido, Juju, and Kai, but Mary’s pet names are the Greek names above.

THE DASHBOARD VIEWS

This is where your “human” (the user) will go to make a new appointment for his or her dog. Note that because we added Devise to the humans table, our authentication is based on current_human.

Using this controller, the Human can create new pets.

rails generate hot_glue:scaffold Pet --namespace=dashboard --auth=current_human

Using the Hawk

Now we’ll make an appointments controller that hawks the pet_id to the current authentication, because the human is the person who owns the pet.

rails generate hot_glue:scaffold Appointment --namespace=dashboard --auth=current_human --hawk=pet_id

This is the shorthand of the hawk definition. When using the short hand, it will be assumed that there is an association pets from the object current_human.

The long form equivalent of the above command is --hawk=pet_id{current_human.pets}. Use the long-form to specify non-standard associations or access control. (See example 4.)

When logged in as Jason, Jason can only see and make appointments for his pets:

When logged in as Mary, Mary can see and make appointments for her pets:

That’s the output hawk. (The hawk guards the drop-down list from displaying beyond the specified scope.)

But the hawk works in both directions too, just in case your end users decide to get a little smart. Let’s suppose Mary is a hacker and knows how to View Source on her browser, find the select element and change the input’s ID to a pet she doesn’t actually own. In this example, Jason owns pet ID 23:

Note that the above fails validation because the pet_id simply gets wiped away by the hawk mechanism.

This produces a non-intuitive “non-failure” when you try the same thing on the update action, but the hawk works just the same.

In this example, Mary got a hold of the view/appointments/_form partial and edits the drop-down to show ALL pets instead of just her pets.

For demonstration, this is what would happen if the hawk was not working for the output (that is, when the list is generated), but still is in place on the input. Notice that Mary tries to set the appointment to pet_id 23 (just as above), which is a pet she does not own. In this case, however, the hawk wipes away the pet_id on the input, but because the appointment already has a valid pet (Cornelius), this does not trigger a validation error because it does not invalidate the record.

Of course, all this “under-the-hood” hacking is intended entirely for the security of your application and has no other purpose other than to guard against smart hackers who could otherwise hijack your foreign keys by setting records to be related to objects they don’t own.

In this example I’ve artificially edit the _form partial to undo the output hawk so that all of the Pet names show up in the drop down list. Notice that the input hawk still works as expected but because the record already has a valid foreign key on it, the hawk does not make the record invalid.

Hot Glue is designed to provide the access control built-in. This natural extension isn’t a complete access control solution, but it fits nicely with the existing access control features.

What we’re doing here is saying that in the context of this controller, this user is allowed access to only these related objects. Of course, if you are going to repeat that pattern across many controllers you will end up with non-DRY access control logic throughout Hot Glue’s controllers.

I use “hard” validations (Rails validations enforced on everybody) to validate that something I know will never happen. (For example, all pets must be owned by a human. ActiveRecord enforces this because by default belongs_to is non-optional.)

I use these kinds of “soft” access controls to set rules around what specific users can do and what objects they will have access to.

Generally speaking, a good portion of Hot Glue builds for me are admin interfaces, where I don’t really have to be concerned at all about hijacking, because admin users can by definition do pretty much anything.

Also, one of the truisms I’ve learned working for high-growth engineering is that rules-based engines that try to anticipate and enforce the businesses’ rules are typically prematurely optimized.

That’s because most business people don’t actually realize how much it costs to “lock in” the business rules into code. Not just in upfront cost— but in dependency lock-in too. I’ve seen whole sets of codified business rules get thrown out the window just weeks after they were architected.

So this provides a ‘thin’ access-control solution that is specific to the concept of access control being specified in this control, which is parallel to how Hot Glue already works (1: user authentication in the base controller; and 2: the *_params method defining input controls to guard against hijacked field input as per the standard Rails strong params setup).


Pet Spa Example App