Most applications have at least two categories of users—administrators and ordinary users. Many applications have finer-grained permissions, but this is a good place to start. Code for this is available in ch14/students008. The first step toward creating an extra category of users is to create a migration with a suitable name:
script/generate migration AddAdminFlagToUsers
In the newly created migration, under db/migrate, change the migration file with the name ending in add_admin_flag_to_users so that it looks like Example 14-4.
Example 14-4. A migration for adding a boolean administration flag to the users table
class AddAdminFlagToUsers < ActiveRecord::Migration def self.up add_column :users, :admin, :boolean, :default => false, :null => false end def self.down remove_column :users, :admin end end
This adds one column to the users
table, a boolean named admin
. It defaults to false
—most users will not be
administrators—and can’t have a null value. Run rake db:migrate
to run the migration and add
the column.
Next, there’s a small challenge. The system really should have at least one administrator, but building a complete user administration facility means building another whole set of forms and controller methods. In addition, it probably makes sense to be able to designate an initial user as administrator before locking all nonadministrators out of the user management system.
For this demonstration, there’s an easy answer—the Rails console. In addition to its use in debugging,
demonstrated in Chapter 11, it’s also a great tool for
making small direct modifications to databases. Run script/console
and at the prompt,
enter:
User.find(:all)
Rails will report a list of user records. You need to find the
id
of the one you want to give
administrative privileges and assign them with something like:
User.find(1).update_attribute('admin', true)
This will find the user
with
the id
value of 1—probably the first
user created—and update the admin
attribute for that record to true
.
You can, of course, pick any id
value
you need for this. If you’re feeling paranoid, you can call User.find(:all)
again to make sure the change
is reflected in the record, or just exit
the console.
Now that users have an administrative flag, and now that there is
at least one user with the admin flag set to true
, it’s time to make that flag matter. One
of the nicest features of restful_authentication
is that you can
centralize many authentication tasks in the app/controllers/application.rb file. Doing
that generally requires overriding the authorized?
method, which is defined in lib/authenticated_system.rb quite simply:
def authorized? logged_in? end
In the examples earlier in this chapter, this was the method that
kept out users who hadn’t created accounts, thanks to the before_filter :login_required
at the top of most controllers. (login_required
is also defined in lib/authenticated_system.rb.)
It’s time to set this to something stronger. An easy approach
would be to add a condition, moving from logged_in?
to logged_in? && current_user.admin?
,
locking out everyone who isn’t an administrator. (It’s always best to
check first that the user is logged in—otherwise,
current_user
will be null, producing
errors when you try to call methods on it.)
Most authentication systems don’t limit content to administrators
while giving users only the privilege of logging in to a useless system,
however. More typically, ordinary users have read access while more
privileged users, like administrators, can make changes. In a RESTful
application environment, this can be easily implemented by letting users
GET anything they want, while only privileged users can use POST, PUT, or DELETE. Rails has a request.get?
method that returns true
for GET and false
for everything else. That makes it
possible to write “must be logged and either GETting something or having
admin privileges” as:
logged_in? && (request.get? || current_user.admin?)
First, this checks to see if the user is logged in. If not, Ruby
won’t bother evaluating the righthand side of the &&
expression, the part in
parentheses, and Rails will reject the request. If the user is logged
in, Ruby next evaluates whether the incoming request was a GET. If it
was, great—there’s no need to evaluate the other side of the ||
expression, and the user is granted access.
If it wasn’t, then Ruby checks to see whether the current_user
object has
its admin
flag set. If yes, then
permission is granted, because admins can make any kind of HTTP call
they want. If not, then permission is denied, because some evil user is
trying to make changes they’re not allowed to make.
Overriding the method call should look like:
protected def authorized? logged_in? && (request.get? || current_user.admin?) end
The protected
label lets objects of classes that descend from ApplicationController
(all your controllers) call the authorized?
method, but prohibits other
objects from calling it. This can go near the bottom of the app/controllers/application.rb file, just
above the final end
statement. Before
closing the file, it also makes sense to uncomment a line just above
this, removing the #
from:
#
filter_parameter_logging :password
Chapter 11 explained filter_parameter_logging
’s blocking of certain
parameters from the logs, and password information is a good choice for
blocking.
At this point you can save the files and call script/server
. If you log in with the account
you set to be administrator (or don’t log in at all), the application
works just like it did before. The more interesting case, of course, is
to create a new user with fewer permissions, and examine the new user’s
experience. Figures 14-8 to 14-11 show how a newly created user steps
through the application.
The beginning of this user’s journey goes as planned. Zoid gets a
screen name (14-8), then visits the students page
(14-9) where all the data is visible.
Feeling curious, though, Zoid tries the Edit link for one of the
students and is rewarded with the editing page (14-10). It’s only when Zoid tries to submit
the edits that the authorization?
method decides it’s time to lock him out (14-11).
From a strictly data security point of view, this is fine. The
user can’t change data without proper authorization. From the user’s
point of view, though, the interface is lying, offering opportunities
that look like they should be available. As an interface issue, this is
a problem with the views and can be solved by checking whether the current
user is an administrator before presenting those options. This needs to
be checked in each of the index.html.erb files for students, courses,
and awards—a little repetition is necessary. The changes, though, are
just a pair of if
statements,
highlighted in Example 14-5.
Example 14-5. Removing inappropriate choices from a user with limited powers
<h1>Listing students</h1> <table> <tr> <th>Given name</th> <th>Middle name</th> <th>Family name</th> <th>DOB</th> <th>GPA</th> <th>Start date</th> <th>Courses</th> <th>Awards</th> </tr> <% for student in @students %> <tr> <td><%=h student.given_name %></td> <td><%=h student.middle_name %></td> <td><%=h student.family_name %></td> <td><%=h student.date_of_birth %></td> <td><%=h student.grade_point_average %></td> <td><%=h student.start_date %></td> <td><%=h student.courses.count %></td> <td><%=h student.awards.count %></td> <td><%= link_to 'Show', student %></td> <td><%= link_to 'Awards', student_awards_path(student) %></td><% if current_user.admin? %>
<td><%= link_to 'Edit', edit_student_path(student) %></td> <td><%= link_to 'Destroy', student, :confirm => 'Are you sure?', :method => :delete %></td><% end%>
</tr> <% end %> </table> <br /><% if current_user.admin? %>
<%= link_to 'New student', new_student_path %><% end%>
This just removes the Edit, Destroy, and New options. (The Awards
entry moves up a line, above Edit, to reduce the number of if
statements needed.) Now, when Zoid logs in
and visits the students page, he’ll see Figure 14-12: just the options
he’s allowed to use.
There are a few other features that need the same treatment, like the link to the form for enrolling students in courses from the app/views/students/show.html.erb file. Every reasonably sophisticated application that has moved beyond the basic CRUD interface will have a few of these cases.
It’s convenient to check results by keeping multiple browser windows open, logged into different user accounts. Remember, though, that Rails is tracking authentication status through sessions, which use cookies, that apply to the whole browser and not just a single window.
The easy way to deal with this is to open two different browsers and log in to the application separately in each of those, rather than in two different windows in the same browser.
There’s still one leftover that may be worth addressing, depending
on your security needs. The authorization?
method has secured the data,
and the view no longer shows the user options they can’t really use, but
if a user knows the URL for the edit form, it will still open. It’s a
GET request, after all. This is a good reason to make sure that these
forms don’t display any information that isn’t publicly available
through other means. If this is an issue, it may be worth the effort of
adding authorization checks to every controller method that could spring
a leak.