April 20, 2009
Older: Twitter Gem Reborn with Fewer Features and 100% More OAuth
Newer: Now With Navigation and Charted Archives
How to Add Simple Permissions into Your Simple App. Also, Thoughtbot Rules!
Last week, in a few hours, I whipped together flightcontrolled.com for Flight Control, a super fun iPhone game. The site allows users to upload screenshots of their high scores. I thought I would provide a few details here as some may find it interesting.
It is a pretty straightforward and simple site, but it did need a few permissions. I wanted users to be able to update their own profile, scores and photos, but not anyone else’s. On top of that, I, as an admin, should be able to update anything on the site. I’m sure there is a better way, but this is what I did and it is working just fine.
Add admin to users
I added an admin boolean to the users table. You may or may not know this, but Active Record adds handy boolean methods for all your columns. For example, if the user model has an email column and an admin column, you can do the following.
user = User.new
user.email? # => false
user.email = 'foobar@foobar.com'
user.email? # => true
user.admin? # => false
user.admin = true
user.admin? # => true
Simple permissions module
Next up, I created a module called permissions, that looks something like this:
module Permissions
def changeable_by?(other_user)
return false if other_user.nil?
user == other_user || other_user.admin?
end
end
I put this in app/concerns/ and added that directory to the load path, but it will work just fine in lib/.
Mixin the permission module
Then in the user, score and photo models, I just include that permission module.
class Score < ActiveRecord::Base
include Permissions
end
class Photo < ActiveRecord::Base
include Permissions
end
class User < ActiveRecord::Base
include Permissions
end
Add checks in controllers/views
Now, in the view I can check if a user has permission before showing the edit and delete links.
<%- if score.changeable_by?(current_user) -%>
<li class="actions">
<%= link_to 'Edit', edit_score_url(score) %>
<%= link_to 'Delete', score, :method => :delete %>
</li>
<%- end -%>
And in the controller, I can do the same.
class ScoresController < ApplicationController
before_filter :authorize, :only => [:edit, :update, :destroy]
private
def authorize
unless @score.changeable_by?(current_user)
render :text => 'Unauthorized', :status => :unauthorized
end
end
end
Macro for model tests
I didn’t forget about testing either. I created a quick macro for shoulda like this (also uses factory girl and matchy):
class ActiveSupport::TestCase
def self.should_have_permissions(factory)
should "know who has permission to change it" do
object = Factory(factory)
admin = Factory(:admin)
other_user = Factory(:user)
object.changeable_by?(other_user).should be(false)
object.changeable_by?(object.user).should be(true)
object.changeable_by?(admin).should be(true)
object.changeable_by?(nil).should be(false)
end
end
end
Which I can then call from my various model tests:
class ScoreTest < ActiveSupport::TestCase
should_have_permissions :score
end
Looking at it now, I probably could just infer the score factory as I’m in the ScoreTest, but for whatever reason, I didn’t go that far.
A sprinkle of controller tests
I also did something like the following to test the controllers:
class ScoresControllerTest < ActionController::TestCase
context "A regular user" do
setup do
@user = Factory(:email_confirmed_user)
sign_in_as @user
end
context "on GET to :edit" do
context "for own score" do
setup do
@score = Factory(:score, :user => @user)
get :edit, :id => @score.id
end
should_respond_with :success
end
context "for another user's score" do
setup do
@score = Factory(:score)
get :edit, :id => @score.id
end
should_respond_with :unauthorized
end
end
end
context "An admin user" do
setup do
@admin = Factory(:admin)
sign_in_as @admin
end
context "on GET to :edit" do
context "for own score" do
setup do
@score = Factory(:score, :user => @admin)
get :edit, :id => @score.id
end
should_respond_with :success
end
context "for another user's score" do
setup do
@score = Factory(:score)
get :edit, :id => @score.id
end
should_respond_with :success
end
end
end
end
Summary of Tools
I should call flightcontrolled, the thoughtbot project as I used several of their awesome tools. I used clearance for authentication, shoulda and factory girl for testing, and paperclip for file uploads. This was the first project that I used factory girl on and I really like it. Again, I didn’t get the fuss until I used it, and then I was like “Oooooh! Sweet!”.
One of the cool things about paperclip is you can pass straight up convert options to imagemagick. Flight Control is a game that is played horizontally, so I knew all screenshots would need to be rotated 270 degress. I just added the following convert options (along with strip) to the paperclip call:
has_attached_file :image,
:styles => {:thumb => '100>', :full => '480>'},
:default_style => :full,
:convert_options => {:all => '-rotate 270 -strip'}
Conclusion
You don’t need some fancy plugin or a lot of code to add some basic permissions into your application. A simple module can go a long way. Also, start using Thoughtbot’s projects. I’m really impressed with the developer tools they have created thus far.
12 Comments
Apr 20, 2009
Hey John, great article, and thanks for checking out our projects.
We’re using your twitter gem in a recent project, and it’s been really easy to figure out (we had search integrated in probably ~30 mins).
Apr 20, 2009
@Matt Glad the love is reciprocal. ;)
Apr 20, 2009
Hi John,
Thanks for a great article, I will have modules in /app/concerns from now on.
Apr 20, 2009
@Jon – Yeah it just depends on what you are doing. I don’t have a hard and fast rule, just go on gut.
Apr 20, 2009
Good stuff. I’m curious about app/concerns—what do you usually put in here? Why not just put it in lib? Sometimes I put modules in app/models if is a module that is only intended to be mixed into models.
I agree that thoughtbot’s tools are great—I use and love shoulda and factory girl. That said, one issue I’ve run into with factory girl is test speed. Creating all your data locally through your models is great, but it’s definitely slower than slamming the data in the database using fixtures, and then relying on transaction rollback between each test to put it back into the known state, so you can re-use your fixture data. This is fine with a small test suite, but as your test suite grows, this becomes more and more of an issue. I decided to do something about it and created factory_data_preloader to address it.
Apr 21, 2009
@Myron Someone smarter than me started calling it that and I liked it I guess. I’ve also seen it in a few projects I’ve worked on and liked it. No grandiose reason in particular.
Apr 22, 2009
Nice work, but what you’ve done can be more powerfully expressed using the acl9 gem:
http://github.com/be9/acl9/tree/master
Works the same way too (mostly).
Apr 22, 2009
@Jaryl – The whole point of this article is simplicity. acl9 may be powerful, but I would definitely not refer to it as simple. It would take me longer to understand it than it took me to implement the code I showed above.
Apr 23, 2009
We’ve just recently started using Clearance on a new Rails project and have found it to be much better than some of the other options. The minor issues we have had with it seem to have been fixed by the new Engine version of the plugin – check it out if you haven’t already.
When we don’t need email confirmation or password reset functionality, we’ve been using the simplest_auth plugin that we (Viget) built. It abides by its label and only handles authentication, so it’s a good alternative to the more full-featured solutions.
Apr 23, 2009
@Patrick Just saw the engine version of it. Looks handy.
Apr 25, 2009
@John – When I first looked at acl9, it went right over my head. I was shopping for a per-object ACL then, so this seemed like the only option, so I went ahead to learn it.
Turns out, using it is much simpler than reading the docs, which are confusing at best. Just my 2 cents.
Apr 29, 2009
Hi John, great article. It’s funny, because I did this exact same thing in one of my projects, but instead used “modifiable_by?”. I even have a draft in my blog explaining this. I just thought it was funny.
One of the things I did different was have 2 methods: accessible_by? and modifiable_by?. In most cases they were one in the same, but in others it would separate the difference between accessing the data in the public and accessing it in the admin. I also had these methods in the class level to determine complete model access.
The one thing I loved about this approach is that it’s pure ruby, no static permission lists or anything like that. It’s plain and simple ruby, so I could use variable conditions in my rules, such as time.
Anyways, hopefully this helps someone.
Sorry, comments are closed for this article to ease the burden of pruning spam.