Bill Katz

My Brain

An occasionally updated repository of thoughts, past work, and links.

Authorization Plugin for Rails

(Updated 6/21) Please see the main Authorization plugin page for updated information.

(Updated 2/22) This treatise describes three aspects of an Authorization plug-in for Rails:

  • a proposed domain-specific language (DSL) for authorization,
  • a pattern for use that describes conventions, and
  • a reference implementation that lets you test the ideas.

The authorization process decides whether a user is allowed access to some feature. It is distinct from the authentication process, which tries to confirm a user is authentic, not an imposter. There are many authentication systems available for Rails, e.g., acts_as_authenticated, salted_password, and LoginEngine. The Authorization plug-in only requires that the authentication system provides a current_user method, which returns an object that fulfills a few methods (explained below). Ruby's duck typing is our friend.

I hope that some subset of Rails programmers likes the DSL for authorization, and by using it in plug-ins, generators, and engines, a part of the Rails community immediately gets benefits from agreeing on a shared language. By separating the proposed DSL and pattern of use from my particular implementation, I hope to foster multiple implementations that support the DSL. For example, my implementation does not handle inheritance of permissions through group memberships, but another implementation could add this feature by checking permissions for all groups associated with a given user. Other authorization systems, like ActiveRBAC or UserEngine, could provide the backend muscle by implementing just a few methods described below.

Since this is a first-pass at an authorization DSL, I realize that it won't be suitable for some programmers' needs and it may have problems for types of authorization. Suggestions for improvement are welcome. We might be able to refine the DSL to handle different ways of authorization in a simple, flexible manner.

I. Toward an Authorization DSL

To check authorization, the system needs to know the user, the roles that are allowed access, and who to query to see if a user has a role. Sensible conventions simplify how we specify the necessary information, and in Rails fashion, the conventions can be overridden.

Here's how the various forms looks in a controller:

 class MeetingController < ActionController::Base
   permit "registered", :except => :public

   def public
     # Anybody can access this action
   end
	 
   def list
     # Any "registered" user can access this action.
     ...
   end
	 
   def add_item
     # Only moderators of this meeting or a user who is an admin can add items.
     permit "moderator of :meeting or admin", :meeting => Meeting.find(params[:id]) do
       ...
     end
   end
	 
   def edit
     # Editing is limited to the admin of this meeting or users who have a global admin role.
     @meeting = Meeting.find(params[:id])
     if permit? "(admin of :meeting) or admin"   # Note that no :meeting hash is provided, so @meeting is used.
       # Do editing
     end
   end

   def edit2
     permit "admin of :meeting" do   # If we've got no @meeting or :meeting hash, we use Meeting.find(params[:id])
       ...
     end
   end
	 
   def stuff
     permit "'grand poohbah' and not 'ruby hater'" do
       # Only reached by users with role "grand poohbah" (two word role name)
       # Denies users who have a role "ruby hater"
     end
   end
	 
   protected
	 
   def assign_moderator
     permit "admin of :meeting or admin"
     ...
     permit_set "moderator of :meeting", :user => @new_moderator
   end
	 
   def remove_moderator
     permit "admin of :meeting or admin"
     ...
     permit_set "not moderator of :meeting", :user => @removed_member
   end
 end

permit and permit? take an authorization expression and a hash of options that typically includes any objects that need to be queried:

   permit  [, options hash ]
   permit?  [, options hash ]

The difference between permit and permit? is redirection. permit redirects by default. permit? can be used within expressions and does not redirect by default.

The authorization expression is a boolean expression made up of permitted roles like "admin" (possible role of current_user), "moderator of :workshop" (looks at options hash and then @workshop), "'big honcho' of :company" (multiword roles delimited by single quotes), or "ruler of World" (queries class method of World).

  • If a specified role has no "of " designation, we assume it is a user role (i.e., queries current_user).
  • If an "of :model" designation is given but no ":model" key/value is supplied in the hash, we check if an instance variable @model if it's available. If @model isn't available, we check for params[:id] and if that's not nil, we ask for Model.find(params[:id])
  • If the model has no preceding colon, we assume it's a class and query Model#self.user_has_role? (the class method) for the permission.

For each role, a query is sent to the appropriate model object.

The grammar for the authorization expression is:

  ::=  | not  |  or  |  and 
  ::= () |  |  of 
  ::= /:\w+/ | /\w+/
  ::= /\w+/ | /'.+'/

Parentheses should be used to clarify permissions. The expression "a or b and c or d" gets parsed like "a or (b and (c or d))", which is a byproduct of the current implementation. Smarter implementations could work left-to-right.

permit_set takes a simple authorization ('role' or 'role of :model'):

 permit_set  [, options hash]

The grammar accepted by permit_set is:

  ::=  | not 
  ::=  |  of 
  ::= /:\w+/ | /\w+/
  ::= /\w+/ | /'.+'/

The reference implementation below does not include permit_set yet.

Options hash

:allow_guests => We allow permission processing without a current user object. The default is false.

:user => A user-style object like current_user.

:get_user_method => A method that will return a user-like object. Default is current_user, which is the way acts_as_authenticated works.

Options hash not used in permit_set

:only => array of methods to apply permit (not valid when used in instance methods)

:except => array of methods that won't have permission checking (not valid when used in instance methods)

:redirect => default is true. If false, permit will not redirect to denied page.

:redirect_action => action that handles authorization failure (default is 'account')

:redirect_controller => controller than handles authorization failure (default is 'denied')

II. Pattern of use

We expect the application to provide the following methods:

#current_user
Returns some user object, like an instance of my favorite class, UserFromMars. A user-like object, from the Authorization viewpoint, is simply an object that provides #roles_include? and #assign_role methods.

UserFromMars#roles_include?(role)
Returns "true" if the given role is held by the current user-like object returned by #current_user.

UserFromMars#assign_role(role)
Associates the given role to an instance of the UserFromMars.

Note that duck typing means we don't care what else the UserFromMars might be doing. We only care that we can get an id from whatever it is, and we can check if a given role string is associated with it.

If you use an authorization expression "admin of :foo", we check permission by sending foo#user_has_role?(user,'admin'). So for each Model that is used in an expression, we assume that it provides the following methods:

Model#user_has_role?(user, role)
Returns "true" if the given role is associated with the given user for this model.

Model#assign_user_role(user, role)

Note two things:

  • Authorization only cares that the user-like model provides the required two methods (#roles_include? and #assign_role). All other required features of the user, like id, are imposed by how you handle role processing on the model side.
  • The user can be nil if :allow_guests => true.

Let's say you don't want permissions distributed across models. You could consolidate permission checking in a variety of ways. For example:

 class ModelWithRoles < ActiveRecord::Base
   def user_has_role?(user, role)
     # code that ties into an authorization system with consolidated permission tables, like ActiveRBAC
   end
   
   def assign_user_role(user, role)
     ...
   end
 end
 
 class Foo < ModelWithRoles
 end
 
 class Moo < ModelWithRoles
 end

The included implementation has a RoleTables module that inserts a default user_has_role? method for ActiveRecord::Base. It shows how you can use role tables with polymorphic associations to easily add roles to models. (See Reference Implementation below)

Conventions

Roles specified without a following "of :model" designation:

  1. We see if there is a #current_user method available that will return a user object. This method can be overridden with the :get_user_method hash.
  2. Once a user object is determined, we pass the role to user#roles_include? and expect a true return value if the user has the given role.

Roles specified with "of :model" designation:

  1. We attempt to query an object in the options hash that has a matching key, as in the MeetingController#add_item example above.
  2. If there is no object with a matching key, we see if there's a matching instance variable, e.g., @meeting for "moderator of :meeting".
  3. If there's no matching instance variable, we see if there's a params[:id]. If so, we use Model.find(params[:id])
  4. Once the model object is determined, we pass the role and user (determined in the manner above) to model#user_has_role?

III. Reference Implementation

An implementation of most of the above DSL and conventions is available as a plug-in (version 0.1.2). Call it alpha. Call it version 0.1. It's under MIT license, so have at it.

My authentication system is derived from Rick Olson's acts_as_authenticated plugin/generator. Code generated from that plugin plays nicely with my plugin.

The reference implementation uses a recursive descent parser that validates a given authorization expression and calls the appropriate permission methods during the parsing. The current plugin does not handle permit_set.

I've included an example RoleTables module that inserts a default user_has_role? method for ActiveRecord::Base. You can easily add roles like 'moderator' and 'member' to models that use polymorphic associations. For example, let's create a moderators table:

class AddModeration < ActiveRecord::Migration
def self.up create_table "moderators", :force => true do |t|
t.column "moderated_type", :string, :limit => 30
t.column "moderated_id", :integer
t.column "user_id", :integer
t.column "created_at", :datetime
t.column "updated_at", :datetime
end
end def self.down drop_table :moderators end end

Note the moderated_type and moderated_id fields used for polymorphic associations. We can moderate any model now. Let's moderate chunky bacon farms.

class Moderator < ActiveRecord::Base
belongs_to :user
belongs_to :moderated, :polymorphic => true
end class ChunkyBaconFarm < ActiveRecord::Base has_many :moderators, :as => :moderated, :dependent => true end

We can check authorization in controllers if our RoleTables module has moderators listed as a default.

class FarmController < ApplicationController
  def sow_salt_into_earth
    permit "moderator of :chunky_bacon_farm" do
      # sow salt into the earth
    end
  end
end

There are many bright folks out there making their own authorization system [1]. This particular system works well for my needs; I'm extracting it from my jumble of code. If you like the approach I'm taking, please contribute ideas, code, testing. Feel free to virtually slap me across the head and say, "Bill, why on earth didn't you do X/Y/Z?" And you will note there is a paucity of tests for this plug-in. In fact, I haven't packaged any tests with the plug-in. Help me rectify this sad state of affairs.

I'm in the process of moving cross-country, so my ability to answer questions will be delayed.

[1] Other efforts include the recently unveiled acl_system from Ezra Zygmuntowicz, the ActiveRBAC project run by Manuel Holtgrewe, and James Adam's UserEngine. ModelSecurity by Bruce Perens adds authorization on the model side. It makes sense to add permit_read, permit_write, and permit_access to model declarations.

Comments are closed

6 Comments

  1. Great Work by Dave Goodlad (2006-02-21)

    This is great, Bill! I implemented a simple role-based authorization system in my app about a month ago, where I do things like: class FooController < ApplicationController authorize :manager, :for => :all authorize :worker, :for => [:list, :show] end It works well, but has some limitations. I was having to explicitly test in before_filters that a user had access to a specific instance of a model, so it just wasn't clean any more; it was halfway there :) I am going to play around with your reference implementation, and see how it works. Looks great so far!
  2. Your wish is easily implemented by Anonymous (2006-04-08)

    Joost, 1. We could use a separate command to remove a role instead of reusing permit_set, as in the current DSL reference: permit_set "not admin". It's a matter of taste and if people think it should be in the DSL, I'd be in favor of calling it something like "permit_unset" just so all the commands begin with "permit" 2. For my particular app, it made sense to use separate tables for the roles. In my app, a moderator could be moderate quite a few distinct models, and I liked having that modeled through polymorphism within that particular role table. Also, I don't have too many roles at this point. But you bring up a good point. There's a very easy modification to my implementation of the DSL that allows a single role table. Simply replace Authorization::RoleTables with an even simpler implementation. Then, when you ask something like -- permit "moderator of :workshop" -- the base ActiveRecord could check one big roles table if the current_user has "moderator" privileges for the given workshop. I'll expand on this when I finally clean up my authorization plug-in.
  3. View Layer by henri (2006-06-07)

    Have you considered how to integrate this in the view layer? One requirements that frequently comes up is to hide unauthorized links. If there were a central location for the authorization info, I could overwrite "link_to". But that information is scattered over the controllers (which is probably the right place for your use-case). The other option is a central config file. Another question is regarding the syntax for method-specific rules. For example: def add_item # Only moderators of this meeting or a user who is an admin can add items. permit "moderator of :meeting or admin", :meeting => Meeting.find(params[:id]) do ... end end Any code between the first and second "end"'s is outside of the permit block, which seems error prone. Other langunages have method-scoped annotations. Are those or anything similar available in Ruby? Thanks, Henri
  4. Re: View Layer by Bill (2006-06-20)

    Henri, Integration in the view layer is quite straightforward. Right now, it's simply the including of the ControllerInstanceMethods module part into ActionView::Base. Then you have access to permit and permit? method within your view. The scoping of the permit block is a little tricky. If the permit isn't approved, it will fire off a redirect. See this article for an explanation. I'm going to move discussion over to the Writertopia developers page once I get commenting set up over there.
  5. Cool post by Justin (2006-02-24)

    Nice post and I like your DSL. Looking at the implementation, though, I wonder why you wrote your own parser? You could also let ruby handle the dsl. Imagine if your blocks actually specified the authorization, instead of just wrapping the permitted code. For example, instead of permit "moderator of :meeting or admin", :meeting => Meeting.find(params[:id]) why not permit do moderator.of(Meeting.find(params[:id])) end or something with boolean expressions: permit do has_role('grand poohbah") and has_role("ruby hater") end Probably not much difference between the two, but it might allow you additional flexibility in the future. I also thought permit_set wasn't very ruby-ish, maybe instead somethign like: permit << "moderator of :meeting", :user => @new_moderator or permissions << "moderator of :meeting", :user => @new_moderator Again, nice post!
  6. Thanks for suggestions by Bill (2006-02-26)

    I went with a homegrown parser because I liked the way the the authorization expression looks. You're right that Ruby lets you get close to the current expressions, but they're still a little too verbose for my taste. I'll have to think about it -- whether the advantages of using straight Ruby is worth the extra has_role's. moderator.of(Meeting.find(params[:id])) is interesting. Since we have to allow arbitrary roles, I guess it would have to be implemented with a method_missing? Food for thought. Thanks for the comment.