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