-
Notifications
You must be signed in to change notification settings - Fork 115
Tutorial: securing a controller
Agenda: we’ve got a standard REST controller (ProductsController) and want to discover how access restrictions can be applied to it (using acl9 of course!)
Here’s the controller (with omitted action bodies for brevity):
class ProductsController < ApplicationController
def index
# ...
end
def show
# ...
end
def new
# ...
end
def edit
# ...
end
def create
# ...
end
def update
# ...
end
def destroy
# ...
end
end
Add a line to the config/environment.rb
file and install the gem.
config.gem "acl9", :source => "http://gemcutter.org", :lib => "acl9"
When everyone is allowed to do everything, it’s also access control. So we’ll start from this.
All access rules in controllers are put into access_control
block. The all-permissive variant will be:
class ProductsController < ApplicationController
access_control do
allow all
end
def index
# ...
end
# ...other actions...
end
allow all
makes sense here, doesn’t it?
This controller should work, if it doesn’t, you might have some problems with acl9 installation.
So, you actually don’t want your competitor to come and delete all your products, replacing them with theirs! A simple scheme could work: you’re the only registered user, you do the stuff, and others are just anonymous users with no rights.
How about this?
access_control do
allow logged_in
end
Seems cool. Only logged in users can… but wait! Now nobody can even see your products (except you)! Something must be done:
access_control do
allow logged_in
allow anonymous, :to => [:index, :show]
end
The second rule to the rescue. In this case anonymous users can see, but not create, edit, or destroy products.
Short answer: not controller.send(:current_user).nil?
.
This means you should use some authentication solution (authlogic, restful_authentication, clearance, or roll out your own current_user
implementation).
Note_: if the method is named differently (current_account
, or even @current_jedi_that_shouldbe@), acl9 can be configured to handle that (see the docs).
Answer: Acl9::AccessDenied
exception is raised.
You’ll probably handle this in the ApplicationController
, in the following manner:
class ApplicationController < ActionController::Base
rescue_from 'Acl9::AccessDenied', :with => :access_denied
# ...other stuff...
private
def access_denied
if current_user
render :template => 'home/access_denied'
else
flash[:notice] = 'Access denied. Try to log in first.'
redirect_to login_path
end
end
end
Let’s imagine your company has had a very successful period, the product count grows every day, but you’re still the only user who can add, or edit products in any way. This task should now be delegated, right? But you’re still afraid that some registered user will come and destroy (intentionally or occasionally) all products, ruining your work.
That is, you want to let other registered users create and edit products, but reserve the destroy for yourself, the admin.
Here you are:
access_control do
allow :admin
allow logged_in, :except => :destroy
allow anonymous, :to => [:index, :show]
end
Now only the admin can do everything, other registered users are not allowed to destroy
.
Short answer: current_user && current_user.has_role?(:admin, nil)
.
You’ll get the has_role?
method in your User
class if you put acts_as_authorization_subject
inside it and create appropriate tables (see the docs).
So far so good. Still, any logged in user can edit products now, even if another user has created them.
And your logged in customers… right! They can edit the desired product, lowering the price to some affordable value, say $0.01, and then buy it. Oh my!
We’ll fix this situation by using object roles.
access_control do
allow all, :to => [:index, :show]
allow :admin
allow logged_in, :to => [:new, :create]
allow :owner, :manager, :of => :product, :to => [:edit, :update]
end
What have we got here? Everyone can go for index and show actions (allow all means just that). Admin can do anything he wants to.
Other logged in users can create new products. But only product owner and product manager are allowed to edit/update.
What does it mean, :product
? Is it a role? Nope. Basically it’s a reference to @product, the instance variable in the controller.
Does acl9 set @product variable automagically? Nope. You’ll need a before_filter
for that.
class ProductsController < ApplicationController
before_filter :load_product, :only => [:edit, :update, :destroy, :show]
access_control do
allow all, :to => [:index, :show]
allow :admin
allow logged_in, :to => [:new, :create]
allow :owner, :manager, :of => :product, :to => [:edit, :update]
end
# ...
private
def load_product
@product = Product.find(params[:id])
end
end
In this case @product variable will be initialized before the access control checks are executed, and that’s exactly what we need!
So, what’s an object role? It’s a role (owner & manager here), tied to a specific object, with specific class and id.
If you use acl9 builtin role subsystem, has_role!
method is the thing. Let’s assign owner role in create
:
class ProductsController < ApplicationController
# ....
def create
@product = Product.new(params[:product])
if @product.save
flash[:notice] = 'Product created.'
current_user.has_role!(:owner, @product) # <--------- assign the role
redirect_to(@product)
else
render :new
end
end
# ....
end
You may unassign a role as well:
current_user.has_no_role! :owner, @product
A @product object might even have several owners this way!
When assigning a role on an object, acl9 will create an entry in the table roles for the given role name (owner), for the authorization_type (Product) and the authorization_id (@product.id). It will also insert a record in roles_users to link the id of @current_user to the role.
So, any registered and logged in user can create his own products now. It’s sorta Web 2.0-ish when your customer creates a product that he wants to buy from you, but let’s explore how to implement a more traditional way to do things.
We want to allow product creation only to selected users, how do we do that? One solution is to introduce product_manager role:
before_filter :load_product, :only => [:edit, :update, :destroy, :show]
access_control do
allow all, :to => [:index, :show]
allow :admin
allow :product_manager, :to => [:new, :create, :destroy]
allow :owner, :manager, :of => :product, :to => [:edit, :update]
end
Destroy right also goes to product_manager. That’ll be enough, but acl9 provides us with a nicer variation:
before_filter :load_product, :only => [:edit, :update, :destroy, :show]
access_control do
allow all, :to => [:index, :show]
allow :admin
allow :manager, :of => Product, :to => [:new, :create, :destroy]
allow :owner, :manager, :of => :product, :to => [:edit, :update]
end
The manager of the Product
class itself! Makes sense, because new
and create
actions operate on Product, not on its instances. Note that Product
manager isn’t automatically a @product
manager (a class role doesn’t always imply an object role).
So, you can assign roles not only for objects, but also for classes:
@good_user.has_role!(:manager, Product)
@bad_user.has_no_role!(:manager, Product)
We’ve successfully enforced access rules on our ProductsController
and finally it qualifies to Mostly High Standards of Security (that’s a joke, you get it).
To top it off, I’ll show you another way to express the same set of rules in acl9 DSL:
before_filter :load_product, :only => [:edit, :update, :destroy, :show]
access_control do
actions :index, :show do
allow all
end
actions :new, :create, :destroy do
allow :manager, :of => Product
allow :admin
end
actions :edit, :update do
allow :owner, :manager, :of => :product
allow :admin
end
end
This may be called action-oriented approach (as compared with the role-oriented approach used before). These two can be mixed together though.
You’re welcome to acl9-discuss Google Group.
- Home
- Role Subsystem
- Access Control Subsystem
- Legacy Docs (some faults/errors may exist)