Topic: Using Model_Security with other models and extending Roles tutorial

I would like to discuss model_security_generator, its functionality, problems with the code, and extending it to use Roles

The official tutorial for model security by Bruce Perrens

Roles with different levels are necessary for most real life web applications, and out of the box model_security only has two roles: nonadmin  and admin.  I have come up with a method to extend model_security to use other roles.

After dissecting the code up and down to figure out how it works and how to extend it, I really like the work Bruce has done!  The system seems great for securing an application and almost complete.

Feedback from others with their success with model_security, any ideas on how to make the code I present more DRY, weakness of model_security's security and ideas on plugging any security holes would be greatly appreciated.

The model_security generator produces great code for securing the user table, but the instructions for how to extend this to other models does not provide complete examples and leave ample opportunity to get it wrong which I did much of.  So I am presenting this tutorial so that you can avoid all those pitfalls.  I hope the following will help those who are interested in model_security. 

Remember the permissions for model_security are defined in the model-  we are securing the data model itself.  My method of including Roles is simple, I just create a module with the role tests for different roles and make it available to the various elements of the application.  The permissions are set for the various roles in each tables model.  I have set this up with a static number of roles.


Using Roles with model_security.

In this example I am extending model_security to include the roles (student, teacher, principal) with different rights based on their role to a table (model) called Thingswith fields id name and user_id.  Scaffold code generated with ruby script/generate scaffold Thing. Table associations added to User model has_many :things and Thing model belongs_to :userCode tested with rails 1.1.6 and 1.2

assuming you have run script/generate model_security -
and cd db/
mysql -u MYSQL-ADMIN-NAME -p < demo.sql
to create the generated user login code and the database

1 Add a character field called role to the user table.

2 Edit the User model /model/user.rb to include :role in atrr_accesible around line 34 and  :role in let_read around line 129

The next file is for holding our tests for the various roles (maybe it belongs as a single table inheritance in the User Class (changing column heading to user_type rather than role and using sti for the classes of different types of users???) anyways- what presented below works as is

3 Create a file /app/helpers/roles.rb to contain the module Roles

module Roles
 
    def owner?
      User.current.id == self.user_id
    end

    def student?
        User.current.role == 'student'
    end

    def teacher?
        User.current.role == 'teacher'
    end

    def principal?
        User.current.role == 'principal'
    end

    def studentorhigher?
        student? or teacher? or principal?
    end
 
    def teacherorhigher?
        teacher? or principal?
    end

    def new?
      new_record?
    end
   
  end


These next few steps are for allowing various parts of the application to access our Roles module.  Is there a single place to add the include Role???  I have had to put it in 4 places as you will see.  Maybe I am missing something.????

4 In /controllers/application.rb add require 'roles' and include Roles

require 'user_support'
require 'roles'                 #this line added to include roles.rb
class ApplicationController < ActionController::Base
  helper :ModelSecurity
  include UserSupport
  include Roles                    #this line added to include Roles module       
  before_filter :user_setup
end

5 In /helpers/application_helper.rb add line
include UserSupport

6 In public part of /lib/model_security.rb around line 42 add line
include Roles

7 In /lib/user_support.rb  line 19 add line
include Roles

8 For Thing model /models/thing.rb add include ModelSecurity and our access and display permissions.  This is where we are setting the access and display permisions for the different roles we created.
class Thing < ActiveRecord::Base
belongs_to :user
include ModelSecurity
include Roles
let_access  :all,  :if => :never? # Set default to don't access, override below.
let_display :all,  :if => :never? # Set default to don't display, override below.
let_access  :all,  :if => 'owner? or teacherorhigher?' #owners and teachers+ can read/write
let_read    :name, :if => :studentorhigher?  #students can see other students things
#let_read   :name, :if => :owner?  #switch to this if only want students to see own things
let_display :name, :if => :studentorhigher? #students and above roles can see all records
let_write   :all,  :if => 'new? or owner? or teacherorhigher?' #save on new, owner & teacher+
end

9 In /controllers/user_controller.rb add @user.role='student'in def new method, before if @user.save (around line 157)
# Create a new user.
  def new
    case @request.method
    when :get
      @user = User.new
    when :post
      p = @params['user']
      @user = User.new(p)

      if UserConfiguration.get.email_confirmation == 0
        @user.activated = 1
      end
      @user.role = 'student'  #or whatever your default code will be
      if @user.save


10 In /controllers/things_controller.rb add  before_filter :require_login as well as code to limit role actions.
class ThingsController < ApplicationController

  #to require that only logged in users can access page
  before_filter :require_login
  before_filter :require_admin, :only => [ :destroy ]
  include Roles
  #to make entire controller limited to certain roles
  before_filter :studentorhigher?
 
  def index
    list
    render :action => 'list'
  end

  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
  verify :method => :post, :only => [ :destroy, :create, :update ],
         :redirect_to => { :action => :list }


  # allow anyone to add new things and create new things
  # but we need to add the user_id to the @thing before its saved

  def new
    @thing = Thing.new
  end

  def create
    @thing = Thing.new(params[:thing])
    @thing.user_id = User.current.id  #set the things user_id to be the current user id before saving
    if @thing.save
      flash[:notice] = 'Thing was successfully created.'
      redirect_to :action => 'list'
    else
      render :action => 'new'
    end
  end

  #to make show only things that belong to the student (or all in case of masterorhigher?)
 
  def show
   @thing = Thing.find(params[:id])
   if !@thing.readable?('name')
     @thing = nil  #just to further secure the app
     flash[:notice] = 'Authorization Failure: You Are Not Authorized to view the record.'
     redirect_to :action => 'list' #not authorized, so just go back to list
   end
  end


  #to make the list only show records associated with the owner or show all in case of teacherorhigher?
 
  def list
   conditions = nil
   conditions = ["user_id LIKE ?", User.current.id] unless teacherorhigher? 
   if conditions
     @thing_pages, @things = paginate :things, :conditions => conditions, :per_page=>10
   else
     @thing_pages, @things = paginate :things, :per_page=>10
   end
  end

  #to make edit only available to recordowner or teacherorhigher can reach the edit page
 
  def edit
   @thing = Thing.find(params[:id])
   if !@thing.writable?('name')
     @thing = nil  #just to further secure the app
     flash[:notice] = 'Authorization Failure: You Are Not Authorized to edit the record.'
     redirect_to :action => 'list' #not authorized, so just go back to list
   end
  end

   #to make is such that only the recordownrr or masterorhigher? can perform a record update

  def update
   @thing = Thing.find(params[:id])
   if !@thing.writable?('name')
     @thing = nil  #just to further secure the app
     flash[:notice] = 'Authorization Failure: You Are Not Authorized to update the record.'
     redirect_to :action => 'list' #not authorized, so just go back to list
    elsif @thing.update_attributes(params[:thing])
      flash[:notice] = 'Thing was successfully updated.'
      redirect_to :action => 'show', :id => @thing
    else
      render :action => 'edit'
    end
  end

  #to make it that only the admincan delete a thing

  def destroy
    Thing.find(params[:id]).destroy unless !theadministrator?
    if !@thing.writable?('name')
      flash[:notice] = 'Authorization Failure: You Are Not Authorized to delete the record.'
      redirect_to :action => 'list' #not authorized, so just go back to list
    else
      redirect_to :action => 'list'
    end
  end
end


11  Modify your views with tests so that innapropriate links are not showing up, such as the 'delete' for a non principal.  Of big importance is to update your views with readable? tests to avoid throwing an security access error (need to check before trying to display data or you will get a security error - the model is protecting your data!.0

For instance, the /views/things/list.rhtml

<h1>Listing things</h1>

<table>
  <tr>
  <% for column in Thing.content_columns %>
    <th><%= column.human_name %></th>
  <% end %>
  </tr>
 
<% for thing in @things %>
  <tr>
  <% for column in Thing.content_columns %>

        <% if thing.readable?(column.name) %>
            <td><%=h thing.send(column.name) %></td>
        <% else %>
        <td></td>
        <% end %>
 

  <% end %>
        <% if thing.readable?(column.name) %>
            <td><%= link_to 'Show', :action => 'show', :id => thing %></td>
        <% else %>
        <td></td>
        <% end %>
        <% if thing.writable?(column.name) %>
             <td><%= link_to 'Edit', :action => 'edit', :id => thing %></td>
        <% else %>
        <td></td>
        <% end %>
   
     <% if principal? %>
    <td><%= link_to 'Destroy', { :action => 'destroy', :id => thing }, :confirm => 'Are you sure?', :post => true %></td>
    <% else %>
      <td></td>
    <% end %> 
  </tr>
<% end %>
</table>

<%= link_to 'Previous page', { :page => @thing_pages.current.previous } if @thing_pages.current.previous %>
<%= link_to 'Next page', { :page => @thing_pages.current.next } if @thing_pages.current.next %>

<br />

<%= link_to 'New thing', :action => 'new' %>


The Thing model is now secure with Roles!!!

SECURITY WORRY
Once an application error is raised within an error in your security code (like if you send a security test to a method that does not exist), the rest of the application continues to function, but now the model_security model side of the security completely goes away (all records viewable and updateable by all) until the server is restarted.  Is there some way to have model security run a test to see if it is working and reinitialize itself if security is down?  This is terrible that if there is an application error on the model_security side the security disappears but the application continues to function.

Try adding to the model let_display :name, :if => :unknownmethod?

The app will error out to an unknown method error.  Then refresh the page and all records will be displayed.  Now logout at /user/logout and login with another user credentials and go back to /things - the model security is still gone.  Seems that the user_support.rb is still functioning but the model_security.rb is down.  How can user_support.rb check on a new login if model_security.rb is not functioning and have it run again????

You can plug the hole for a bad method or test by modifying the code of /lib/model_security.rb around line 9 to include a begin the test code...rescue return false end construct around the test running code so that at least the application will not error out and die.

    def run_test t
      begin
       case t.class.name
       when 'Proc'
        return t.call(binding)
       when 'Symbol'
        return self.send(t)
       when 'String'
        return eval(t)
       else
        return false
       end
      rescue
        return false
      end
    end

HTTP authentication annoyance

If the HTTP authorization part of model_security_generator is annoying you as it did me  (I could not get it to log out properly, kept logging back in via http after logout, to logout, I would have to submit blanck login and password via http multiple times) 

Disable http authorization in /controllers/user_controller.rb comment out line 108

#http_authorize

you can also rip out the /lib/user_support.rb plugins ability to login via http by commenting out the lines 147, 148, 152, 153, 154 and 155

The app will now use form based login

Create Your Form Elements To Work Properly With The Plugin

Also note that without going into the user_support.rb code, you have to form your forms the normal way, not the form for way as model_security does not know how to parse the form for constructs

Can't say:

<% form_for(:thing, :url => thing_path, :html => { :method => :put}) do |f| %>
    <%= f.text_field :name %>...

Must say:
<%= start_form_tag :action => 'update', :id => @thing %>
   <%= text_field 'thing', 'name'  %>...

Hope all this information helps you!!!! 

Cheers Jason
Odyssey Expeditions Tropical Marine Biology Voyages

Last edited by odysseyjason (2007-01-27 23:07:20)