I recently added attachment_fu to a personal Rails project that was already using GateKeeper and ran into some stumbling blocks. It took me some time to dig into attachment_fu's inner workings and some experimentation to figure out the best way to get to the two plugins to play nice together, so here's some tips for anyone else who might try this themselves someday.

attachment_fu is a Ruby on Rails plugin by Rick Olson that facilitates file uploads and is the successor to Rick's acts_as_attachment plugin. GateKeeper is a Ruby on Rals plugin by yours truly that provides a natural language DSL to easily setup and manage highly complex RBAC permission rules at the model level.

I'm going to use 'Picture' as my attachment_fu model for these examples. Typically with attachment_fu, this model would probably look something like...

class Picture < ActiveRecord::Base
  belongs_to :user
  has_attachment :content_type => :image, :max_size => 50.kilobytes, :thumbnails => {:thumb => 'x42'}
end 

Now, with gatekeeper, you might expect to just add a couple simple permissions so the owner can create, update and destroy the picture and share it with other users, such as....

class Picture < ActiveRecord::Base
  belongs_to :user
  has_attachment :content_type => :image, :max_size => 50.kilobytes, :thumbnails => {:thumb => 'x42'}
  
  ## Permissions ##
  crudable_by_my_user
  readable_by_anyone
  #################
  
end 

Note that the readable_by_anyone permission assumes that you have a 'is_anyone?' User instance method that always returns true.

There are two problems with the above. First, you're going to have issues the first time you try to access the thumbnail version of the image. Second is that the above is using the default :db_file storage method which actually adds a another ActiveRecord model, and which GateKeeper will immediately lock down tight. If you're not using thumbnails or the :db_file storage method, then the above is probably all you need. Everybody else, keep reading.

First, let's address the thumbnail issue, since that's the easiest to deal with. In order to support thumbnails, attachment_fu's instructions tell you to add a 'parent_id' column to your 'pictures' table. attachment_fu then automatically provides a 'parent' association on thumbnails that point back to the original uploaded image. You're going to want thumbnails to just inherit their GateKeeper permissions from the original image, so that anyone with permission to read or delete the original image can do the same to the thumbnails. To do this, just add a 'crudable_as_my_parent' permission to Picture. (Notice that that says as_my_parent, not by_my_parent.) This will allow anyone who can read the original to also read the thumbnail, and anyone who can destroy the original to also destroy the thumbnail.

class Picture < ActiveRecord::Base
  belongs_to :user
  has_attachment :content_type => :image, :max_size => 50.kilobytes, :thumbnails => {:thumb => 'x42'}

  ## Permissions ##
  crudable_by_my_user
  readable_by_anyone
  crudable_as_my_parent
  #################
end 

Ok, that was actually pretty easy. The :db_file storage method is just a little more challenging. If you're using :filesystem or :s3 as your storage medium for uploaded files, then you should be fine since neither of these use extra ActiveRecord models to access the files, so GateKeeper won't get in the way. However, if you're using :db_file, here's what you want to do. First, create a db_file.rb file in /app/models and make it look something like this (changing all three (3) occurances of 'picture' to match whatever your attachment_fu model is).

class DbFile < ActiveRecord::Base
  has_one :picture
  crudable_as_my_picture
  crudable_by_anyone :if => lambda {|db_file| db_file.picture.nil? } #pic already destroyed
end

The first line provides an association for GateKeeper to use to get back to the picture model. The second line tells DbFile to inherit it's GateKeeper permissions from the picture provided by the association. The third line is the tricky one. When pictures are destroyed, Rails will destroy the picture first, then automatically try to destroy the associated db_file (via a ':dependent => :destroy' call). However, when it goes to delete the db_file, there won't be a picture anymore to inherit permissions from, and line two will fail. We can safely assume though that if a db_file is being destroyed and if (and only if) the picture doesn't exist, it too must have just been destroyed by the current user and the db_file should follow suit. You should never have stray :db_file objects left in your database whose associated objects are gone, so there's really no risk in allowing anyone to destroy one in such an orphaned state.

We're almost done, but not quite yet. The last problem is that the DbFile class you just defined will never get loaded by Rails. attachment_fu is going to create it's own DbFile and the permissions you setup will never get loaded or used. So, let's go back to the Picture class and add one line to the very top to force your DbFile class to load before attachment_fu tries to create it's own.

require_dependency 'db_file'
class Picture < ActiveRecord::Base
  belongs_to :user
  has_attachment :content_type => :image, :max_size => 50.kilobytes, :thumbnails => {:thumb => 'x42'}

  ## Permissions ##
  crudable_by_my_user
  readable_by_anyone
  crudable_as_my_parent
  #################
end 

And that's it. GateKeeper and attachment_fu should play nice together now, giving you file upload wonderfulness and model security peace of mind. One last thing I'd like to briefly touch on is the possibility of having more than one attachment_fu model. Let's say you're running a job site that allows users to upload a photo of themselves AND a pdf resume. So, you're going to have both a Picture and a Resume class, and DbFile is going to need associations to both. Note that I haven't actually tried this, but updating your DbFile class that you built above to the following should work just fine. (Continue the pattern for any additional attachment_fu models you want to add to your application.)

class DbFile < ActiveRecord::Base
  has_one :picture
  has_one :resume
  crudable_as_my_picture
  crudable_as_my_resume
  crudable_by_anyone :if => lambda {|db_file| db_file.picture.nil? and db_file.resume.nil? }
end
Comments: 0 [add comment]

I've noticed in my site logs people are searching the Interweb Tubes for "GateKeeper" combined with other keywords that suggest they're looking for some better examples on how to use it, so I figured maybe I'd write something up to try to help.

GateKeeper is a Ruby on Rails plugin providing a natural language DSL to manage security permissions on instances of ActiveRecord classes at the Model level. Permissions may be assigned based on the current users roles, and/or by their relationships to the object in question. GateKeeper automatically intercepts all attempts to CRUD (Create, Read, Update & Destroy) instances of ActiveRecord classes and verifies the current user has permission before allowing the operation to proceed.

The main requirement of GateKeeper is that it expects that you have a 'User' class, and that it responds to the class method 'current', as in 'User.current'. If you need to use a different class (such as 'Person' or something), that can be defined in your environment.rb, but whatever it's called, it must respond to 'current' and return an object that represents the currently logged in user. So, here's a simple example of how to set that up.

Exactly how you get your users logged in is up to you and still beyond the scope of this post, but I'd recommend a SessionsController and a User.login(username, password) method. So, we'll assume that you have that setup already and that you store the 'id' of the currently logged in user in session, such as 'session[:user_id]'.

The first thing you'll want to do is have a method in your application controller that sets up the current user for the rest of the application...

class ApplicationController < ActionController::Base
  before_filter :setup_user

  def setup_user
    User.current = User.find(session[:user_id])
  end
end

Now, we need to add 'current' and 'current=' methods to User.

class User < ActiveRecord::Base
  
  ## Class Methods ##
  class << self
    
    def current
      @CURRENT_USER || User.new(:username => 'guest')
    end

    def current=(u)
      @CURRENT_USER = u
    end

  end

end

And that's it for User.current. Now, anywhere in your application (Models, Controllers, Helpers, and Views), you can just call User.current, and get an object that represents the currently logged in user. Now we'll go into some basic uses of GateKeeper.

If users can register new accounts on your site, then you'll need to allow 'guest' users permission to create new users. Update your User class with the following (previously shown class methods left out for brevity).

class User < ActiveRecord::Base
  ## Permissions ##
  creatable_by_guest
  #################

  def is_guest?
    username == 'guest'
  end
end

However, if at any point your application tries to save the current user, it could potentially end up creating a user with the username 'guest', which you probably don't want. So, update the above with the following...

  ## Permissions ##
  creatable_by_guest :unless => lambda {|new_user| new_user.is_guest? }
  #################

The 'is_guest?' method above is an example of how to define dynamic 'user roles' for GateKeeper. When you say 'creatable_by_guest', GateKeeper expects to either find a 'is_guest?' instance method on User, or a Role named 'guest' belonging to User.current (see below). Here are some examples of other dynamic user roles that you might want to define for your users.

class User < ActiveRecord::Base
  def is_premium_member?
    premium_membership_expires_on > Time.now
  end
  
  def is_logged_in_user?
    !is_guest?
  end

  def is_popular_user?
    popularity_ratings.size > 42
  end
end

class Foo < ActiveRecord::Base
  ## Permissions ##
  creatable_by_premium_member
  updatable_by_popular_user
  readable_by_logged_in_user
  #################
end

As I stated above, GateKeeper also looks for a Role belonging to User.current with the name of the Role used in the permission. To set this up, you'll want the following...

class CreateRoles < ActiveRecord::Migration
  def self.up
    create_table :roles do |t|
      t.string :name
      t.timestamps
    end
    add_index :roles, :name
    
    create_table :roles_users, :id => false do |t|
      t.integer :user_id
      t.integer :role_id
    end
    add_index :roles_users, :user_id
  end

  def self.down
    drop_table :roles
    drop_table :roles_users
  end
end

class User < ActiveRecord::Base
  has_and_belongs_to_many :roles
end

class Role < ActiveRecord::Base
  has_and_belongs_to_many :users
end

Now, if you were to create a role named 'moderator', and assign it to a few of your users, you could give those users special permissions on certain models, such as updatable_by_moderator . Take note that the role name should be all lower case and underscored so that your permissions look nice. If you have a role named 'SpecialUser', then your permissions would need look like 'readable_by_SpecialUser' which isn't beautiful code, and if the role name has spaces in it, it won't work with GateKeeper at all. 'special_user' is a much more appropriate role name for GateKeeper's purposes.

So, there you have it. I hope that helps some people get over the initial hurdle of setting up GateKeeper. I'd love to hear from anyone using GateKeeper and hear your thoughts and questions.

Comments: 0 [add comment]

GateKeeper 0.2 is now available. This version traverses eagerly loaded associations and checks read permissions on each loaded object for the current user. Rdocs and install instructions available at gatekeeper.rubyforge.org.

GateKeeper is a Ruby on Rails plugin providing robust Model level permissions management, allowing you to declare access permissions for instances of ActiveRecord classes with natural language commands. For instance

  • crudable_by_admin
  • creatable_by_my_author
  • updatable_by_friends_of_my_owner :unless => :is_imaginary?
  • updatable_by_monkey_of_my_uncle :if => :has_common_ancestor?
  • readable_by_employee_of_the_month
  • and a whole lot more.
Comments: 0 [add comment]
Announcing GateKeeper

I'm pleased to announce the official release of my latest Rails plugin "GateKeeper". GateKeeper started life as a library in one of my big personal Rails projects where I've been experimenting with multiple methods of setting, managing and enforcing access and security permissions for specific models. This project has lots of users with different security access levels to other user's data based on their roles and relationships to each other. I think this latest experiment was a huge success, so I immediately decided to give it away to you ('cause you're all that and more).

Why GateKeeper?

In most simple and straight forward Rails projects, scoping your finds through has_many associations is probably sufficient for ensuring that users can't get at data they aren't supposed to. For example, a show action might call @current_user.notes.find(params[:id]) to ensure that the current user owns the note with the passed id, or it won't even find it.

However, things quickly get more difficult when users need limited access to other user's objects based on different roles (such as Admin, Moderator, Premium Subscriber, Employee of the Month, etc), or based on their relationship with the object's owner (such as Supervisor, Teacher, Parent, etc). You might want to allow variety of people to read certain objects based on a variety of different roles and relationships, and only allow a subset of those people to update or destroy the same objects. Trying to manage all of this can be daunting, and before long your code starts to look like a mess and it becomes easier and easier to accidentally leave a hole open that allows the wrong people to see or even modify data they're not supposed to. Adding new features that will require a new layer of permissions has now become the thing nightmares are made of and you start to wonder when in the hell Rails stopped being fun.

GateKeeper to the rescue!
  • GateKeeper allows fine grained control of CRUD permissions on individual instances of any ActiveRecord Model.
  • GateKeeper lets you define these permissions in plain English in one central location right in each model.
  • GateKeeper lets you give permissions based on the current users roles (RBAC), and/or based on the current user's relationship with the model instance in question via ActiveRecord associations.
  • GateKeeper allows permissions based on associations to be easily inherited from other classes through multiple chained associations.
  • GateKeeper strictly enforces every CRUD action that occurs on every instance of every ActiveRecord model, making sure no users ever do anything that permission hasn't been explicitly granted for.
  • GateKeeper can optionally scope all finds to only return records that the current user has 'read' permission for.
That's a lot of reading! How about some samples instead?

Sure, how about this?

class User < ActiveRecord::Base
  has_many :articles
  has_and_belongs_to_many :roles
  
  ## GateKeeper Permissions ##
  # Grouped together for easy reference later
  crudable_by_admin
  creatable_by_guest :unless => lambda {|new_user| new_user.username == 'guest'}
  updatable_by_self
  ############################
  
  #etc...
end

class Article < ActiveRecord::Base
  belongs_to :author, :class_name => 'User'
  
  ## GateKeeper Permissions ##
  crudable_by_admin
  creatable_by_my_author
  readable_by_my_author
  updatable_by_my_author :unless => :published?
  readable_by_premium_member :if => :published?
  ############################
end
Ok, I'm sold! What do I need to do to get started.

Well, first, install the plugin. script/plugin install svn://rubyforge.org/var/svn/gatekeeper/trunk

Next, by default GateKeeper assumes that you have a class named 'User' that represents individual users on your site. If not, you'll need to add the following line to your environment.rb to tell GateKeeper the name of the class you use for users. GateKeeper.user_class_name = 'Person' Through the rest of this post, mentally replace any mention of "User" or "User class" with whatever class name you set this to.

Lastly, GateKeeper requires that your User class provide a 'current' class method (eg. User.current)that returns an instance of User representing the user currently logged into the site. How you implement this is based entirely on your method of keeping track of who is logged into your site. (If you ask nice enough, I can probably be talked into doing a future post to outline how I do it.)

How can I do Role Based Access Control (RBAC) with GateKeeper?

By default GateKeeper makes a bunch of assumptions about your User class. If any of this won't work for your application, you can easily override it (details at the end of the section).

If you declare a permission such as updatable_by_admin, then whenever a user tries to update an instance of the class, GateKeeper performs a couple of checks.

First, GateKeeper checks to see if instances of your User class respond to is_admin?, and if so, calls that method on User.current. If that method returns true, the update is allowed to proceed.

If the above method returns false (or isn't defined), then GateKeeper checks to see if instances of your user class respond to roles (provided by an association like has_and_belongs_to_many :roles), and if so calls User.current.roles.find_by_name('admin') to see if your user has a role with the name defined in the permission declaration. If it does, the the update is allowed to proceed.

If both of the above two checks fail, then the update is halted and a permission error is raised.

If for any reason that won't work for you (i.e. you use some other method to check users roles), then you can simply define an instance method named has_gate_keeper_role? that takes one string argument which will be the name of the roles GateKeeper wants to know about, and will return true or false. Precisely what you do inside has_gate_keeper_role? is entirely your business.

How do permissions based on associations work?

When you declare a permission such as updatable_by_my_owner, GateKeeper treats the 'my_' prefix on the role as a flag to indicate it needs to check associations on the object in question (instead of a user's role as described above). Whenever a user tries to update an instance of the class with this permission, GateKeeper expects to find a owner method (presumably provided by an association, but not necessarily) that returns an instance of User. GateKeeper then simply compares the object returned by 'owner' with User.current and if they math, the action is allowed to proceed.

Can a model "inherit" some or all of it's permissions from some other model?

Absolutely, thanks to associations and the '_as_' preposition. Let's say you have an instance of Chair, which belongs_to :table. If you declare updatable_as_my_table in your Chair class, then anyone who has update permission of the specific table that your instance of chair belongs to can also update the chair. You may find that the permission declaration that you use most often is simply crudable_as_my_<association>, which will cause instances of the class to inherit all of their permissions from the associated object.

What if the association isn't a user?

Often times associations refer to some other class with it's own associations, and to get from your current object to an instance of User you need to travel through several associations (eg. @chair.table.room.house.owner ). GateKeeper lets you dictate how to get from the current object to an instance of User in either of two styles.

First, you can simply chain multiple 'crudable_as_*' permissions across multiple associations. In the example above we had an instance of Chair inheriting it's update permissions from the instance of Table it belonged to. If in turn Table declared updatable_as_my_room then Chair and Table would both get their update permissions from Room.

Alternatively, you can declare the entire chain directly in Chair with the '_of_' separator, and you can chain as many of them toegther as you like, so you might do something like updatable_by_owner_of_house_of_room_of_my_table as long as each class has the correct associations setup so that @chair.table.room.house.owner returns an instance of User to compare to User.current.

Additional Conditionals!

In addition to all of the above, permission declarations may also be given an ':if' or ':unless' argument that takes a string, symbol, or proc that will return a boolean, allowing you limitless flexibility in dictating when certain actions are allowed to occur.

What's this "permission scoping" thing you mentioned earlier?

By default, if any of the ActiveRecord finder methods find an object that User.current doesn't have permission to read, GateKeeper will throw a permission error. Even with permission scoping turned on, this is still true any time the finder method is expected to return an exact number of objects (eg find(1,3,5,9)).

However, any ActiveRecord finder methods that typically return arrays of arbitrary length (eg. find(:all)), GateKeeper allows you to tell it to just return those that User.current has permission to read, instead of throwing an error for those they can't. This can be enabled in one of two ways.

First, you can turn permission scoping on globally for your application by setting GateKeeper.permission_scoping_enabled = true in your environment.rb. Or, if you prefer to leave permission scoping off most of the time, you can simply pass a block to GateKeeper.with_permission_scoping and that block will be executed with permission scoping enabled just for it.

Note however, that it is still strongly advisable to limit your find in some way. Since GateKeeper still needs to iterate through each and every item returned by the finder to to check if it's readable, running GateKeeper.with_permission_scoping { Article.find(:all) } is likely going to be a very long running command if you have thousands, or even just a couple hundred, articles in your database. If this happens to you, work on making your block return fewer records in the first place, such as with extra conditions, limits, or paginating.

More information?

More information, including installation instructions, can be found in the online documentation at rubyforge. Enjoy, and please let me know how it goes. You can post questions or comments either here, or on the Ruby on Rails mailing list (be sure to include the word "GateKeeper" in the body to catch my attention.)

Comments: 0 [add comment]