Saturday, January 31, 2009

Plug-ins, Security and Frustration

Really nothing to post on except my increasing level of frustration with Rails security plug-ins and my domain model. This shouldn't be that difficult given:
  • There are multiple clinical facilities
  • Users can belong to more than one facility
  • Users can have different roles at each facility
  • The application supports an HTML interface as well as a REST API
Authentication was easy, I used the restful_authentication plug-in. It is easy to customize and works great with HTTP basic authentication and SSL for securing the REST API. The only modification I made to the plug-in was changing from SHA1 to SHA512 for the password hashes.

I looked at a number of authorization plug-ins and ended up using restful_acl. It works well with restful_authentication and basically lets you implement you authorization logic as you see fit. My models are:
class User < ActiveRecord::Base
  has_many  :user_roles,      :dependent => :destroy
  has_many  :security_roles,  :through => :user_roles
  has_many  :facilities,      :through => :user_roles
  has_many  :messages
  has_one   :user_profile,    :dependent => :destroy
end

class UserRole < ActiveRecord::Base
  belongs_to :facility
  belongs_to :user
  belongs_to :security_role
end

class Facility < ActiveRecord::Base
  belongs_to  :facility_type
  belongs_to  :state
  belongs_to  :country

  has_many    :user_roles
  has_many    :users, :through => :user_roles
  has_many    :messages
end

class SecurityRoleRight < ActiveRecord::Base
  belongs_to :security_right
  belongs_to :security_role
end

class SecurityRole < ActiveRecord::Base
  has_many :user_roles
  has_many :users, :through => :user_roles
  has_many :security_role_rights, :dependent => :destroy
  has_many :security_rights, :through => :security_role_rights
end

class SecurityRight < ActiveRecord::Base
  has_many :security_role_rights, :dependent => :destroy
  has_many :security_roles, :through => :security_role_rights
end
Some of my resources are nested and some are not. I used the resources_controller plug-in to simply the handling of the nested resources. For example:
# Facilities
map.resources :facilities do |facilities|
  facilities.resources :messages
end

# Messages
map.resources :messages

# You can have the root of your site routed with map.root
map.root :controller => 'messages', :action => 'index', :resource_path => '/messages'
Now throw in that the web application needs to support the concept of a current facility. Easy enough as a user has a primary_facility flag in the UserRole model. After authentication, this information can be stored in the session along with the current user. At this point I could easily finish creating my web application.

The UI could include a drop down list of facilities that the user can access. When the user makes a selection from the list, the session can be updated with the newly selected facility. My authorization code can use the facility id in the session to determine if the user can access the model. I tested this by modifying the restful_acl plug-in to pass the current facility to the authorization check methods:
def is_readable_by(user, facility, object = nil)
  return false unless
    user.facilities.detect { |fac|
      user.security_roles.detect{ |role|
        role.security_rights.detect{ |right|
          fac.id == facility.id &&
          ((right.action == 'index' && object == nil) ||
           (right.action == 'show' && object != nil)) &&
          right.model == self.to_s
        }
      }
    }
  return true
end
The problems started when I tried to apply authorization rules to the REST API. Now it is true that the first time a user access the rest API with a session, the current facility context is set. This approach fails if the user access the API in the following manner and their primary facility is something other than 1:
curl http://localhost:3000/facilities/1/messages -H "Accept: text/xml" -u guest:password -c cookies.txt -b cookies.txt
I removed the current facility from the session and pursed some other options for obtaining a facility context on the fly. None of following are very good as I keep finding situations where the solution doesn't work:

REST API users need to access the URI that points to the facility they want to work with to set a current facility context. This reverts back to my original solution above.
  • PRO: Same logic to set current facility works in web application and REST API
  • PRO: Works non-nested and nested resources
  • CON: Doesn't seem to be very RESTful, easy to get contexts crossed with the session and URIs
Get the facility from the request parameters collection.
  • PRO: Easy to obtain and works great with nested resources
  • CON: Doesn't work with non-nested resources
  • CON: All resources would need to be nested under facilities, could easily break the 1 level nesting "rule"
Get the facility from model associations. If a user is trying to access a message, the message's parent (facility) is looked up first and the passed to the authorization check method.
  • PRO: Works with nested and non-nested resources
  • CON: Requires an additional instance method on each model to find it's parent facility
  • CON: Show action is straight forward, index becomes complicated almost easier to bypass restful_acl and filter in the controller
I am starting to think that I have overcomplicated what I am trying to. I need to go back and re-evaluate what other plug-ins are out there or roll my own. I have posted this dilemma on rubyonrails-talk without much luck. If anyone can offer some advice on the subject, I would be grateful.