Code Cowboy

Dan Croak

I’ve written before about lightweight access control using Inherited Resource’s begin_of_association_chain and raising a 404.

I still prefer this approach in the vast majority of apps I work on, but it doesn’t work in the case where there is no relationship between a user and an object other than access control.

gunshot

custom

Last week, I had a need to differentiate access control to some controller actions based on whether a signed in user was a teacher or student.

This is a simple need, and I’m loathe to introduce a large gem into the app for what could just be a custom before_filter:

class CoursesController < InheritedResources::Base
  actions :new, :show, :index

  before_filter :authenticate
  before_filter :deny_student, :only => [:new]

  protected

  def deny_student
    deny_access if current_user.student?
  end
end

However, I recently saw acl9 mentioned on a mailing list and really liked the DSL. Couldn’t we have the best of both worlds?

So, Josh Clayton and I wrote up a sub-set of acl9’s DSL that would meet my needs.

module AccessControl
  def self.included(controller)
    controller.extend(ClassMethods)
    controller.send(:include, InstanceMethods)
  end

  module ClassMethods
    def access_control(options = {}, &block)
      before_filter(options) do |controller|
        controller.authenticate
      end

      before_filter(options) do |controller|
        controller.instance_eval(&block)
      end
    end
  end

  module InstanceMethods
    def allow(role, opts = {})
      if opts[:to].nil? || opts[:to].include?(action_name.to_sym)
        unless current_user.send("#{role}?")
          deny_access(opts[:flash])
        end
      end
    end

    def deny(role, opts = {})
      if opts[:from].nil? || opts[:from].include?(action_name.to_sym)
        if current_user.send("#{role}?")
          deny_access(opts[:flash])
        end
      end
    end
  end
end

ActionController::Base.send :include, AccessControl

Gunshot

the DSL

In action:

class CoursesController < InheritedResources::Base
  actions :new, :show, :index

  access_control do
    allow :teacher, to: [:new]
  end
end

Very expressive. Nice work, Oleg Dashevskii!

strut

features we cared about

  • optional flash mesage
  • natural language (allow/to and deny/from)
  • role is just a boolean on User

opinions we could have

This will usually be included when most actions require a signed in user. Therefore, we call authenticate and allow an ugly override for edge cases:

class CoursesController < InheritedResources::Base
  actions :new, :show, :index

  access_control(:except => :show) do
    allow :teacher, :to => [:new]
  end
end

This is used with Clearance and can therefore rely on authenticate, current_user, and deny_access.

instance_eval

Writing code with a pretty DSL in mind is exceedingly fun. The one downside seems to be that scope can become confusing.

Yesterday I was messing with Rails app templates and read the source for the 2.3 template runner. It, like this custom access control DSL, also uses instance_eval as the key line to make things work.

In the app template example, it was hard to figure out how to get the file path of the original template.

I simply wanted to require 'helper', a separate file in the app template, but because of the scope within which it was instance_eval‘d, helper could not be found.

in_root { self.instance_eval(code) }

The solution in that case was to take advantage of other public methods and mess with the load path, which would probably an atrocious solution for a vendored gem in a Rails app, but acceptable for a one-off script to generate a Rails app:

here = File.expand_path(File.dirname(template), File.join(root,'..'))
$LOAD_PATH << here
require 'helper'

In the access control example, it was hard to figure out how to have access to flash, redirect_to, etc. from a class-level scope.

Similar to the require 'helper' example, your tests will fail if:

  • deny and access are defined at the class level
  • instance_eval is not used to delay evaluation until runtime
  • you don’t use before_filter’s block

The solution is to use blocks and lazy evaluation to control scope, but it can be confusing to get there:

before_filter(options) do |controller|
  controller.authenticate
end

before_filter(options) do |controller|
  controller.instance_eval(&block)
end

not a code cowboy

Every time you decide to roll your own, remind yourself that you may waste time getting lost in something like a scope problem you haven’t seen before. Then, once you get the rhythm down, be careful of a thought like “it won’t take me that long to write.” Re-inventing the wheel has to be balanced with a specific reason in addition to, “it will be fun for me.”

The third-party ecosystem of gems and plugins is one reason why Rails is awesome. However, writing custom code every now and again is worth it for programmer pleasure, fewer lines of code and dependencies, and staying focused on only what you need.

Ruby makes great DSLs, though, so don’t be afraid to take inspiration from existing code and try your hand at making beautiful code for a specific purpose.

Visit our Open Source page to learn more about our team’s contributions.