Rails: How to Reduce Friction at the Authorization Layer
9 min read

Rails: How to Reduce Friction at the Authorization Layer

๐Ÿ’ก That's when it hit me. Yes or no are core to authorization. But what if I also included why?

It all started so innocently. I knew that I wanted something to organize authorization for Flipper Cloud. But I wasn't sure what. Pundit? Why not.

I started slapping policies in and peeling out. Over time though, I noticed the same things that I notice on every app I work on โ€“ entropy and friction at the authorization layer.

Steve and I were chatting about it one day over a screen pairing session and he said something to the effect of "Hey, wouldn't it be nice if we had a message in addition to true/false." ๐Ÿ’ก

Yeah, yeah that would be real nice Steve. And you know what, that would be real possible too. So I started pulling at the string and pretty soon I had this big, beautiful ball of ๐Ÿงถ that was the envy of every ๐Ÿˆ around.

I've shared snippets of it with friends and they seemed pretty interested, so why not a full on blurgh post.

Authorization != REST

The first string I pulled was the awkwardness of policy names matching REST actions (e.g. index, create and show). This is Ruby. Ruby is a conversation.

policy(organization).view_members? is a conversation. policy(User).index? is not.

policy(Project.new).create? knows nothing other than the type of instance it is. policy(organization).create_project? knows a lot more.

Here I passed in a class. There I passed in an instance of a class. Some of those instances were persisted records. Others were not.

I couldn't keep it straight. Do I need to check new_record? or persisted? here or not? Am I dealing with a class or an instance of a class?

Incongruence

I felt incongruence. Incongruence isn't always obvious. But you feel it. Deep in your programmer bones you feel it.

Notice how some code just feels right and you rock and roll and are on top of the world? But other code requires you to continually re-check a file for the name of a method or parameter or what type of object you are dealing with?

That is incongruence. If it isn't flowing, that means the code is showing (I rhyme all the thyme). Its showing you that something is not right. That is what I kept feeling.

So that was step #1. It might feel minor, but for me it opened up the flood gates of authorization.

Adios in-memory

I changed code like this:

if policy(@organization.projects.new).create?

To code like:

if policy(@organization).create_project?

Oooh, that feels nice ๐Ÿ˜Ž. Just like that all-my in memory objects were gone. Again, such a small thing, but something just felt right.

Adios classes

I didn't stop there though. Next I swapped out code like:

if policy(OrganizationMembership).index?

For something more readable and enforceable like:

if policy(organization).view_members?

Every object passed into a policy was a persisted instance. Congruence!

Conquer the Repetition

The next string that I tugged on was all the repetition of checking organization or project membership. Most policies had something like this in them:

class TokenPolicy < ApplicationPolicy
  def show?
    project_member?
  end
  
  def update?
    project_member?
  end
  
  def destroy?
    project_member?
  end
  
  private 
  
  def project_member?
    record.project.member?(user)
  end
end

Solving this was mostly delegation plus extract method to superclass. I made sure that every object under the umbrella of a Project or an Organization delegated up the chain.

class Environment < ApplicationRecord
  delegate :organization, to: :project
end

class Feature < ApplicationRecord
  delegate :organization, to: :project
end

class Gate < ApplicationRecord
  delegate :project, to: :feature
  delegate :organization, to: :project
end

class Webhook < ApplicationRecord
  delegate :project, to: :environment
  delegate :organization, to: :project
end

Doing this made it so that project_member? could move up to ApplicationPolicy:

class ApplicationPolicy
  def project_member?
    record.project.member?(user)
  end
  
  def organization_member?
    record.organization.member?(user)
  end
end

Things were starting to feel better. The permissions were more descriptive (not REST) and DRY. But something still didn't feel right.

The Message

The core of Pundit, or most authorization related code, is can someone do something โ€“ true or false. But neither true nor false tell the whole story.

Any designer or developer worth a lick will typically let a software user know why they aren't allowed to do something. This is really what Steve was getting at in our conversation at the beginning of this post. Sorry for taking so long to get to the ๐Ÿ–.

Predicate Methods

The message of why a user can't do a thing was the tricky part. My code was littered with create_project?, invite? and other predicate methods.

Predicate methods in Ruby always return true or false. To return some other object (that had message and result) would be confusing. It would require every developer who works on this project to learn something new and totally different than idiomatic Ruby.

No, that just wouldn't work. So what was I to do?

What if each predicate method had a companion method that wasn't a predicate and returned some sort of result object.

That method could also be memoized and re-used in the predicate method. Memoizing would avoid incurring a duplicate check of authorization for a single predicate check and output of a message.

Let's look at a sample:

<% if user_policy.create_organization? %>
  <%= link_to "Add an Organization", [:new, :organization] %>
<% else %>
  <span title="<%= user_policy.create_organization.message %>" data-toggle="tooltip">
    Add an Organization
  </span>
<% end %>

I mean that reads ok, right? If the user_policy can create an organization, then the Add an Organization link is shown. If it cannot, then I instead show a tooltip with a title explaining why an organization cannot be created.

No more running checks in policies and then running checks again in a controller or view to figure out why a given policy was false.

No more generic messages like "You do not have permission to do this."

Because we have an exact message for why the authorization failed, we can tell the user exactly why.

The Code Migration

So I slapped a DSL in ApplicationPolicy that made it easy to define both methods at once.

The best part? Because the predicate methods all stayed the same, no view or other code had to change immediately.

Over time, I picked spots where having more information on why a user couldn't do something would be more valuable and started using the predicate and non-predicate method calls in combination to provide more information.

The DSL

I know, I know, you just want to see my instance_eval, catch / throw and other terrible things.

But before we get to that, can we just admire this:

class ProjectPolicy < ApplicationPolicy
  policy :show, :update, :destroy, :view_features, :view_members, :view_environments, :create_environment do
    require_project_membership!
  end

  policy :create_feature do
    require_project_membership!
    require_subscription_access! message: "Creating a feature requires a trial or subscription."
  end

  policy :invite do
    require_confirmed_user! message: "Inviting collaborators is disabled until you verify your account by email."
    require_organization_membership! message: "Only organization members can invite collaborators."
  end

  policy :cancel_invite do
    require_organization_membership! message: "Only organization members can cancel invitations."
  end
end

Maybe I'm just too familiar with this because I wrote it.

But I'm pretty sure you can read the code above and understand the permissions for managing projects in Flipper Cloud โ€“ even if you aren't sure how to use it.

The Response Object

The first layer of this ball of yarn is a PolicyResponse class. This is what is returned by the non-predicate method and wraps up all the data we'll need.

class PolicyResponse
  attr_reader :reason, :message

  REASON_TO_MESSAGE = {
    organization_membership_required: "You must be an organization member.",
    project_membership_required: "You must be a project collaborator or an organization member.",
    subject_memberbership: "You must be a project collaborator or an organization member.",
    subscription_required: "You must have a subscription.",
    anonymous_user: "You must be signed in.",
    unconfirmed_user: "You must have a confirmed email address.",
  }.freeze

  def self.true(reason = nil, message = nil)
    new(true, reason, message)
  end

  def self.false(reason = nil, message = nil)
    new(false, reason, message)
  end

  def initialize(value, reason = nil, message = nil)
    @value = value
    @reason = reason
    @message = message
  end

  def can?
    @value
  end

  def cannot?
    !can?
  end

  def title
    @title ||= (@reason.to_s.presence || "Whoops!").humanize
  end

  def message
    @message_text ||= @message.presence || REASON_TO_MESSAGE.fetch(@reason, "You are not authorized.")
  end
end

Nothing fancy. There is a value, a reason and a message. The value is true/false. The reason is typically a short hand symbol representation of "why not?" when the value is false. Lastly, the message is a more human friendly version of the reason.

You might have also noticed I added a couple of factories (is that the right fancy enterprise programmer lingo?) at the top for true and false responses and some default message values based on the reason. These are all just to save me a few key strokes when defining policies.

The DSL Method

The next layer on this path to authorization bliss was a tiny DSL in ApplicationPolicy.

class ApplicationPolicy
  extend Memoist

  # Define a new policy and the predicate method.
  #
  # Example:
  #   policy :show do
  #     PolicyResponse.new project_member?, :project_membership_required
  #   end
  #
  # Returns nothing.
  def self.policy(*names, &block)
    names.each do |name|
      define_method(name) do
        catch(:halt) { instance_eval(&block) }
      end
      memoize name

      define_method("#{name}?") do
        send(name).can?
      end
    end
    nil
  end
end

There it is. Yep, catch, instance_eval, memoize and all kinds of other shameless tactics to reduce friction and start to enjoy authorization again. I really threw the kitchen sink at this.

Anyway, this adds a class method that I can then use in subclasses to define policy methods. This is akin to how belongs_to, has_many and other Active Record methods exist in your models.

I should also note that Memoist is gem I often use. It is extended here which adds the memoize method used in the class method.

You already saw how the policy class method works (at the opening of this section), but didn't know where it came from.

Here's a short example as a refresher:

class ProjectPolicy < ApplicationPolicy
  policy :update, :destroy, :view_features, :view_members, :view_environments, :create_environment do
    if project_member?
      PolicyResponse.true
    else 
      PolicyResponse.false :project_membership
    end
  end
end

Policy methods that share permissions can be defined together. That means in the aforementioned example that update, destroy, view_features, view_members, view_environments, and create_environment all share the same permission check.

Inside the block you can do whatever Ruby you need to determine if a user is able to do something or not. The only requirement is that a PolicyResponse is returned.

Pour some sugar on it

Pretty much immediately I got tired of repeating if/else and returning PolicyResponse instances. So I added a few more methods to ApplicationPolicy to DRY up the repeated permissions.

class ApplicationPolicy

  # [snipped for brevity]

  private

  def has_subscription_access?
    record.organization.has_subscription_access?
  end

  def organization_member?
    record.organization.member?(user)
  end

  def project_member?
    record.project.member?(user)
  end

  def require_confirmed_user!(message: nil)
    require_signed_in_user!
    deny! :unconfirmed_user, message unless user.confirmed?
    PolicyResponse.true
  end

  def require_signed_in_user!(message: "You must be signed in.")
    deny! :anonymous_user, message if anonymous_user?
    PolicyResponse.true
  end

  # Only allow if organization has subscription access.
  def require_subscription_access!(message: nil)
    deny! :subscription_required, message unless has_subscription_access?
    PolicyResponse.true
  end

  # Only allow project members.
  def require_project_membership!(message: nil)
    require_signed_in_user!
    deny! :project_membership_required, message unless project_member?
    PolicyResponse.true
  end

  # Only allow organization members.
  def require_organization_membership!(message: nil)
    require_signed_in_user!
    deny! :organization_membership_required, message unless organization_member?
    PolicyResponse.true
  end

  # Wherever we are at in the decision process, stop and allow access.
  def allow!
    halt! PolicyResponse.true
  end

  # Wherever we are at in the decision process, stop and deny access.
  def deny!(reason, message = nil)
    halt! PolicyResponse.false(reason, message)
  end

  # Stop trying to figure out if someone can or cannot do something and just
  # return this policy response.
  def halt!(policy_response)
    throw :halt, policy_response
  end
end

Thanks to the delegation (covered a few sections ago) and stolen catch / throw from Sinatra (source), my policies started bringing me joy.

Enough joy that I'm writing this here post for you. That doesn't usually happen when I write authorization code.

The previous project membership check could now be expressed as:

class ProjectPolicy < ApplicationPolicy
  policy :update, :destroy, :view_features, :view_members, :view_environments, :create_environment do
    require_project_membership!
  end
end

With the PolicyResponse object, the class method and the accompanying helpers I can whip together permissions really easily now.

Sharing and Customizing

I also ensured that policies could be re-used within a method.

For example, I wanted most of the permissions for gates (the ways to enable / disable a feature) to be the same. But I wanted an extra limit on the number of actors or groups enabled for a feature to avoid mis-use.

class FeatureWithEnvironmentPolicy < ApplicationPolicy
  policy :manage_gates do
    require_subscription_access!
    require_project_membership!
  end
  
  policy :clear, :mirror, :enable, :disable, :disable_actor, :disable_group, :set_percentage_of_actors, :set_percentage_of_time do
    # All of these individual gate operations should also check manage gates.
    # Think of manage gates as a super permission check that has to be passed
    # first. Then, any specific gate permission checks have to pass too.
    manage_gates
  end

  policy :enable_actor do
    if record.flipper_feature.actors_value.size >= Limit.actors_count_limit
      deny! :actor_limit, "This feature has reached the limit to the number of actors per feature. Check out groups as a more flexible way to enable many actors."
    end

    manage_gates
  end

  policy :enable_group do
    if record.flipper_feature.groups_value.size >= Limit.groups_count_limit
      deny! :group_limit, "This feature has reached the limit to the number of groups per feature."
    end

    manage_gates
  end
end

๐Ÿ’ฅ All feature gate changes are controlled by manage_gates. But enable_actor and enable_group do an extra check for the set size.

Vanilla pundit can even be used in the controller via the authorize method.

class GatesController < ApplicationController
  def enable
    authorize @feature_with_environment, :enable?
    @flipper_feature.enable
    redirect_to [current_organization, @project, @environment, @feature]
  end
end

Because authorization became so easy and flexible, I started using the policies in new ways. Check out this :api policy method I added to the TokenPolicy.

class TokenPolicy < ApplicationPolicy
  policy :api do
    deny! :not_found, "Environment could not be found." unless record.environment
    deny! :not_found, "Project could not be found." unless record.project
    deny! :not_found, "Organization could not be found." unless record.organization

    require_subscription_access! message: "Using the API requires a trial or subscription access. Visit: #{Rails.application.routes.url_helpers.organization_subscription_url(record.organization)}"

    unless record.enabled?
      deny! :disabled, "Disabled tokens cannot access the API."
    end

    PolicyResponse.true
  end
end

Then, in a middleware before every API request, the policy is checked:

policy = token.policy.api
unless policy.can?
  if policy.reason == :subscription_required
    return error_response(request, 402, policy.message)
  else
    return not_found(request, policy.message)
  end
end

Note that I customize the response code based on the reason. If I'm dealing with a subscription issue, I can return a payment required status code.

Wrapping Up

Is any of the above a perfect abstraction? No. But it removed the friction I was feeling to the point that I was excited to show boring policies to friends, and to you dear reader.

I think the TLDR is that vanilla pundit, a mindset change, and a wee bit of code authorization moved from annoying to empowering.

Whew. That was a long one, but I hope you enjoyed it and maybe learned a little something. It took me around 4+ hours to write.

If you did enjoy or learn anything, you should probably check out Flipper Cloud. Sign up, kick the tires and let me know what you think. I crave feedback.

If you enjoyed this post,
you should subscribe for more.