ModelSecurity helps Ruby on Rails developers implement a security defense in depth by implementing access control within the data model.
If you are like most developers, you think about security when you program controllers and views. But a bug in your controller or view can compromise the security of your application, unless your data model has also been secured.
The economical, flexible, and extremely readable means of specifying access controls provided by ModelSecurity makes it easier for the developer to think about security, and makes security assumptions that might otherwise live in one developers head concrete and communicable to others.
Defense in depth is a common military strategy to
prevent an attack from succeeding. If the attacker gets through
the first level of defense, there's another level there to meet
him. Defense in depth is useful to programmers because it will
reveal a flaw for repair in the first level while simultaneously
stopping the attack at the second level. Most developers
implement some security features in their controllers and views:
for example, a controller will generally not allow access to
administrative features unless the user is logged in as an
administrator, and your views should not display data that is
inappropriate for a particular user. This is the "first layer of
defense". At that point, we come to the data model access
permissions: your second layer of defense. If you've specified
them, ActiveRecord will not allow the data to be revealed or
modified in an inappropriate manner. Views will in general
quietly fail to present non-permitted material in a way that can
be exploited: it will be blank if it's not readable, and will be
presented as static data rather than in an edit field if it's not
writable. This behavior facilitates scaffolding, which would
display and allow modification of everything if not told
otherwise. A non-permitted controller access to the data model
will result in a SecurityViolation exception being
raised, which causes the action to terminate and logs diagnostic
information that will assist the developer to locate the
controller bug.
ModelSecurity adds these facilities to Rails:
let_read, let_write, and
let_access are added to model declarations. They
allow fine-grained security permissions to be specified for
each model attribute, or for all of them at once.true
indicates that the access should proceed while
false causes it to be denied. This mechanism
allows any user authorization system to be wired into
ModelSecurity with only a few statements.let_display facility
that works similarly to the security specifications, and tells
Rails scaffolds and views what fields of a data model are
"interesting" and what fields need not be shown to a particular
user.User_setup is a before-filter for the
entire application. It handles login control and session
management without additional attention from the
programmer.require_login and
require_admin ensure that a user logs in with
proper privilege and then will complete the requested
action.Before we go through the features of ModelSecuity, it's useful to get working code on your system. To go through this demo, you'll need to have Rails, RubyGems, and MySQL installed.
Generate a rails program called test, and change to its directory:
rails test cd test
Install the model_security gem:
gem install model_security_generator
Generate model_security for your application
script/generate model_security -
Have MySQL execute the script to create the database and a MySQL user ID for the rails program:
cd db/ mysql -u MYSQL-ADMIN-NAME -p < demo.sql
Examine demo.sql and users.sql . users.sql defines the table of users. An accessory table defined by user_configurations.sql is used to hold configuration choices for the user system. When you include ModelSecurity in another Rails program, you'll skip demo.sql and use users.sql and user_configurations.sql to create their tables.
Edit the database configuration in db/database.yml to look like this:
development: adapter: mysql database: model_security_demo host: localhost username: m_s_demo password:
Add some necessary facilities to your brand new application controller:
cd ../app/controllers cp ADD_TO_APPLICATION_CONTROLLER.ModelSecurity application.rb cd ../..
If this wasn't a new controller, you would edit application.rb to add the facilities requested, rather than copying over the file.
Start the rails server:
script/server &
Open a web browser to http://localhost:3000/user/new Use the form to create a new user. That user will automatically be granted the administrator role, and will be activated immediately.
Subsequent users will not automatically get the administrator role, although an administrator can grant it to them by editing their record with /user/edit/user-name . Subsequent users will not have their login activated immediately when they create it. Instead, they will be sent an email containing a URL that they must use to activate their login.
Now, you can play with the demo. The user controller responds to:
/user/activate/id?token=security-token/user/configure/id/user/destroy/id/user/edit/id/user/forgot_password/user/list/user/login/user/logout/user/new/user/showNow that you have the software running, create a user using
/user/new The first user that you create will
automatically be granted the administrative privilege, sort of
like "super-user" status on a Unix or Linux system, and will be
logged in immediately.
You should be able to grant administrative status to subsequent users by editing their record and setting the admin field to 1, due to a Rails bug (at this writing, October 2005), the scaffold views aren't capable of editing boolean database fields. We won't need that facility to go through our demo.
Log out of the application via the /user/logout action. Your browser will present a login panel using HTTP authentication. Use the cancel button or whatever mechanism your browser provides to get rid of that panel. You should now see a login form in an HTML page. Don't log in.
Run the /user/list action. This action requies a
login before it will complete. The browser will present the login
panel again. Log in to the application as your administrative
user. You'll now see the list of users.
Let's look at the code that made that login panel happen. In app/controllers/user_controller.rb, you will find this code:
# Require_login will require a login before the action can be completed. # It uses Modal, so it will continue to the action if the login is # successful. before_filter :require_login, :only => [ :list, :show ]
That code causes require_login to be called before the
list or show actions. Require_login
will keep trying to get a user logged in until the login succeeds
or the user bails out of the process using the back button. If
you don't yet understand how before-filters work, you'll
need to peruse the Rails documentation. If a user is already
logged in, require_login returns true and execution
proceeds. If no user is logged in, require_login
will save the current action and then redirect the browser to the
/usr/login action. Once the user is logged in
successfully, the browser will be redirected to the previous
action.
Although you might believe that all of this is taking place
sequentially in your Rails application, that's not the case. It's
achieved by sending a redirect to the browser as each step
completes. At each step the browser requests the URL it's been
redirected to, and Rails carries out an individual action.
Execution proceeds this way if the user asks for
/user/list while not logged in:
/user/list/user/login/user/login/user/list/user/listrequire_login as a before-filter for any action that
should only be executed for logged-in users.
There's also a require_admin filter. This one is
used with the destroy action of the user controller,
as shown by this code:
# Require_admin will require an administrative login before the action # can be called. It uses Modal, so it will continue to the action if the # login is successful. before_filter :require_admin, :only => [ :destroy ]
That will prevent the destroy action from being
executed unless the user successfully logs in as an
administrator.
require_admin and require_login are
part of your controller-based security setup. As the first line
of defense, they will be where potential security violations
should stop.
There's only a little more to learn about logging in and
users. Take a look at the file
app/controllers/application.rb . You'll see these
lines:
require 'user_support'
class ApplicationController < ActionController::Basehelper :ModelSecurity
include UserSupport
before_filter :user_setupend
Requiring the file user_support and including the
module UserSupport mix the login and user-management
capabilities into the application controller's class. The
before-filter user_setup handles session control and
login management for the entire application. Once
user_setup has run, the User object for
the currently-logged-in user can be accessed through
User.current .
That's all you need to know to use the User
system to manage logins. But we'll keep looking at the
User code to see how it secures its own data using
ModelSecurity.
Create a second user with /user/new . This user
won't be privileged. The system will send you an email with
instructions on how to confirm that user's login. Just in case
email delivery isn't working on your server, you can get the
email text out of the log file logs/development.log
. Confirm the user's login.
Logged in as your administrative user, visit
/user/list . That will display a list of the users
you've created so far. Because you are the administrator, their
emails will be visible to you. Now, log out using
/user/logout and log back in as the non-privileged
user you created. Visit /user/list again. Now,
you'll be able to see your own email, but not that of other
users.
Let's look at the code that makes that happen. Edit
app/models/user.rb and find these lines:
# If this is a new (never saved) record, or if this record corresponds to # the currently-logged-in user, allow reading of the email address. let_read :email, :if => :new_or_me_or_logging_in?
This code, in the declaration of the User class, says that the
email attribute of the model can be read if the
function new_or_me_or_logging_in? returns true.
Let's look at that function:
# Return true if the user record is new (never been saved) or if it # corresponds to the currently-logged-in user, or if the current user # is the special "login" user. This security test is a common pattern # applied to a number of user record attributes. def new_or_me_or_logging_in? new_record? or User.current == self or logging_in? end
The first condition is the Rails function
new_record? which would return true if the record
has never been saved. The second condition would return true if
the record refers to the currently-logged-in user, and this is
what allows the user to see his own email address. The third
condition returns true if there is currently no logged-in user
(it contains the expression User.current == nil).
These are all of the cases in which the application should allow
reading of the email attribute of a
User record, except one.
NOTE: at this time, there is a app/views/user/list.rhtml template installed by the generator that overrides the scaffold. The difference between this file and the scaffold template is that ActiveRecord#readable? is used to determine if a datum should be displayed or not. This file will move to app/views/scaffolds/list.rhtml once I figure out how to override the scaffold template location. - Bruce
What tells the software that the administrator can read
everyone's email address? That's another line in the
User model declaration:
# Let the administrator access all data. This implements a Unix-like # super-user. Note that the coarse-grained override of the super-user # is not a _necessary_ pattern for the ModelSecurity module, you can # implement controls as fine-grained as you like. let_access :all, :if => :admin?
let_access calls let_read and
let_write with the same arguments, and thus sets
both read and write permissions. :all is a special
name that means the permission applies to all of the attributes
of the data model, not just one database field. And
admin? is a function that returns true if the
currently-logged-in user has the administrative privilege. It
just tests the value of a model attribute called
admin. The effect of all of this is that the
administrative user can read or write any datum.
You must start by setting the permissions for
:all in your model if you are using ModelSecurity,
as the default is to permit all accesses. If you won't be setting
up an administrative user, as above, it would be a good idea to
make the default "no access". You can do that with the following
code:
# Set the default to "no access". let_access :all, :if => :never?
The never? test is provided with ModelSecurity,
and always returns false. A companion test
always? reliably returns true.
ModelSecurity has two mechanisms for protecting data. One
protects the actual data model and will throw a exception upon an
unpermitted access. But that's not what we've been seeing: our
views have quietly refused to display some unpermitted data,
without any exceptions. A second mechanism prevents the common
view functions, including those used by templates, from
displaying or editing data inappropriately. You activate that
mechanism by declaring ModelSecurityHelper in your
application, as below in
app/controllers/application.rb:
require 'user_support'
class ApplicationController < ActionController::Basehelper :ModelSecurity
include UserSupport
before_filter :user_setupend
This makes the code in ModelSecurityHelper
available to all views in the application. ModelSecurityHelper
redefines these common view functions to respect the data model
permissions:
One complication of the view protection is that instance variables of your model that are not related to database fields are effected in the same way as model attributes. So, when you declare the accessors for those instance variables with code like this:
attr_accessible :password, :password_confirmation, :old_password
You should also declare security permissions on those same variables:
# NOTE: :password, :password_confirmation, and :old_password # are not attributes of the record, they are instance variables of the # class and aren't written to disk under those names. But I declare them # here because otherwise ModelSecurityHelper (which doesn't know that) # isn't going to allow me to enter them into a form field. # # I like how fine-grained I can get. let_write :password, :if => :new_or_me_or_logging_in? let_write :password_confirmation, :if => :new_or_me? let_write :old_password, :if => :me?
You may also have noticed that there are more fields in the
database, and more attributes in the User object, than the view
displays when you visit /user/list . But a quick
perusal of the code will show that it's a scaffold view. You
would have expected a scaffold to display all fields. But
ModelSecurity allows us to override that with the
let_display declaration. Take a look at its use in
the User model declaration:
# These just control display. let_display :all, :if => :never? # Set default to don't display, override below. let_display :admin, :activated, :if => :admin? let_display :login, :name, :email
This declaration will cause the scaffold view to present
information about three fields, login,
name and email to all users (although a
let_read declaration further restricts whose email
address a non-privileged user can see, as above). Administrative
users will also see the fields admin and
activated. Nobody will see the fields
cypher and salt, they are long strings
of encrypted or random cybercrud that nobody wants to read. Note
that the scaffold view will format the display as a table with
columns only for the data that it will display. All of this is
achieved by overloading the content_columns method
of ActiveRecord to filter the columns it reports
using the data in the let_display declaration.
While the tests used with let_read,
let_write, and let_access are instance
methods of your ActiveRecord class, tests used with
let_display must be class methods. This is because
the decision of what columns to display is made before the first
row's instance is accessed. Rails makes it easy enough to declare
a method to work with both an instance and its class.
You've now completed the ModelSecurity tutorial. For more information, see the reference.