Example 3: Hawking Foreign Keys to the Current User (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 within that context, let’s say you have another foreign key — somewhere else in your data model, and that foreign key should also 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 you build, your users will have full access to all records in the database for non-authentication foreign keys. (That is, foreign keys that aren’t part of the existing access control provided by the authentication or 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.

This app is somewhat involved, so we’ll dive right in with the setup commands and then quickly move on to the important features.

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

Be sure to add the line inflect.irregular "human", "humen"


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:






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

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.



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.)


Here’s what the hawk does:

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



Hot Glue hawking foreign keys protects displaying records not owned by the logged in user

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:

Notice that Mary cannot hack the interface with a hijacking attack to set pet_id to a pet that doesn’t belong to her. Here, she tries to set pet_id to 23, but since pet_id is hawked to the current user (her) she isn’t allow to set that key. Because this also makes the record invalid, the new record is not created.

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 simply 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 edited 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 already in place.

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 would end up with non-DRY access control logic throughout Hot Glue’s controllers.

I recommend using “hard” validations (Rails validations that are enforced on everybody) to validate that something I know will never happen. (For example, all pets must be owned by a human. This is enforced by ActiveRecord 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.

This provides a ‘thin’ access-control solution that is specific to the concept of access being specified in this controller, 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).

Complete and Continue