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