Welcome to Giant Robots Smashing Into Other Giant Robots — a weblog about development, business, design and technology — written by thoughtbot.
Second refactoring of security roles
A common issue we have in larger applications is ensuring that the right logged in users see the right resources (and don’t see those they aren’t supposed to). We visited a solution to this in a previous post, which worked fine at the time. Our project has since grown, as projects are wont to do, and the wrapper class solution just wasn’t scaling. See below for our most excellent second solution…
A little refresher on what not to do…
The wrapper class approach is just fine for a single resource that needs security applied to it. For a refresher, here’s the model…
1 2 3 |
class Article < ActiveRecord::Base # Columns include submitted and accepted end |
And here’s the wrapper that you would use in its place when in an Admin controller…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class AdminArticle def self.method_missing(method, *args) scope = { :find => { :conditions => ["submitted = ?", true] }} Article.with_scope(scope) do # we've limited the scope, so just pass the method call on... Article.send(method, *args) end end def self.new(*args) # need to explicitly pass this on self.method_missing(:new, *args) end end |
And here’s the wrapper that you would use in its place when in a Member controller…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class MemberArticle def self.method_missing(method, *args) # We get current_user through a class-level accessor that I've left out for sanity scope = { :find => { :conditions => ["approved = ? or user_id = ?", true, current_user] }} Article.with_scope(scope) do # we've limited the scope, so just pass the method call on... Article.send(method, *args) end end def self.new(*args) # need to explicitly pass this on self.method_missing(:new, *args) end end |
You can already see how this isn’t gonna scale as you add more AR classes that need security? Not to mention the fact that it violates the newly private nature of ActiveRecord.with_scope. Finally, it’s not nearly as purty as I’d like. We all like purty, right? Lemme see some hands!
That’s much better!
First, the library…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
module Filterable def self.included(clazz) clazz.class_eval do extend ClassMethods end end module ClassMethods def scope_to_role(role, user = nil) case role when :public scope = { :find => { :conditions => ["approved = true"] } } when :admin scope = { :find => { :conditions => ["submitted = true"] } } when :member raise RuntimeError, "#{self.name}.scope_to_role(:member, nil) called, which doesn't make sense" unless user scope = { :find => { :conditions => ["approved = true OR user_id = ?", user.id]} } else raise RuntimeError, "#{self.name}.scope_to_user called with unrecognized role #{role}" end with_scope(scope) do yield end end end end |
Then the models…
1 2 3 4 |
class Article < ActiveRecord::Base include Filterable # Columns include submitted and accepted end |
And using it in the controllers…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class ApplicationController < ActionController::Base around_filter :filter_results_for_public def filter_results_for_public People.scope_to_role(:public) do Article.scope_to_role(:public) do Photo.scope_to_role(:public) do yield end end end end def filter_results_for_admin People.scope_to_role(:admin) do Article.scope_to_role(:admin) do Photo.scope_to_role(:admin) do yield end end end end def filter_results_for_member People.scope_to_role(:member, current_user) do Article.scope_to_role(:member, current_user) do Photo.scope_to_role(:member, current_user) do yield end end end end end |
1 2 3 4 |
class Admin::BaseController < ApplicationController skip_filter :filter_results_for_public around_filter :filter_results_for_admin end |
1 2 3 4 |
class Member::BaseController < ApplicationController skip_filter :filter_results_for_public around_filter :filter_results_for_member end |
There’s a downside?
The biggest downside is that you’re yielding about six times for each and every request. You could trim that down by excluding certain actions, but it’s still not the most efficient design.
Also, we could probably get all clever and combine those filter_results_for* methods into one which introspects on the url or current controller. Do we want to do that? I didn’t think so.
About this entry
You're reading an entry on GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS, the company weblog of thoughtbot, inc.
- Author:
- Tammer Saleh
- Published:
- January 12th 11:31 AM
- Updated:
- January 12th 11:33 AM
- Sections:
- Development
thoughtbot is hiring
We are hiring web developers and web designers in both Boston and New York, NY.
What are we up to?
We built Shoulda, an eclectic set of additions to Test::Unit; Paperclip to manage uploaded files without hassle; Jester, a REST/ActiveResource client library written in Javascript, and Squirrel, an enhancement for ActiveRecord's find syntax; — amongst some other projects.

Chad (President) and Jon (CTO) co-authored a technical book titled Pro Active Record: Databases with Ruby and Rails, which explores the ins and outs of the ActiveRecord ruby library. You can buy it today at Amazon.com.
About thoughtbot, inc.
We are a small web application development consulting business, with offices in Boston, MA and New York, NY. If you're looking to find a team for your next web development project or your new web application — get in touch.
0 comments
Jump to comment form