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.
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
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!
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
andaccess
are defined at the class levelinstance_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.