views:

68

answers:

3

I'm shifting code from an application built in a non-standard custom PHP framework into Ruby on Rails (version 3). In the PHP version all the controllers are really fat, with thin models, which I've always disagreed with, so I'm enjoying the way Rails does validation at the model level, which is probably 90% of what's happening in these fat controllers currently.

One problem I'm facing, and unsure how to resolve however, is that of differing validation rules based on who's making the change to the model. For example, an administrator, or the original creator of the record should be able to do things like flag a record as deleted (soft delete) whereas everybody else should not.

class Something < ActiveRecord::Base
  ...
  validates :deleted, :owned_by_active_user => true
  ...
end

class OwnedByActiveUserValidator < ActiveModel::EachValidator
  validate_each(record, attr_name, attr_value)
    # Bad idea to have the model know about things such as sessions?
    unless active_user.admin? || active_user.own?(record)
      record.errors.add :base, "You do not have permission to delete this record"
    end
  end
end

Since the model itself is (in theory) unaware of the user who is making the change, what's the "rails way" to do this sort of thing? Should I set the active user as a virtual attribute on the record (not actually saved to DB), or should I just perform these checks in the controller? I have to admit, it does feel strange to have the model checking permissions on the active user, and it adds complexity when it comes to testing the model.

One reason I'm keen to keep as much of this as possible in the model, is because I want to provide both an API (accessed over OAuth) and a web site, without duplicating too much code, such as these types of permissions checks.

+2  A: 

It is really the controller's job to handle authorization, or to delegate authorization to an authorization layer. The models should not know about, nor have to care about, who is currently logged in and what his/her permissions are - that's the job of the controller, or whatever auth helper layer the controller delegates that to.

You should make :deleted in-attr_accessible to mass assignment via new, create, or update_attributes. The controller should check the authenticated user's authorizations separately and call deleted= separately, if the authenticated user is authorized.

There are several authorization libraries and frameworks to help with authorization or to function as an authorization layer, such as cancan.

Justice
Thanks, I have to agree with you. My models should do as they're told (provided the business logic allows them to). My controllers should decide who tells them. I'll just make sure I abstract away the gory details as best I can.
d11wtq
+1  A: 

I would solve this with a before_filter in my controller, instead of with validations in my model.

class SomethingController < ApplicationController
  before_filter :require_delete_permission, :only => [:destroy]

  def destroy
    # delete the record
  end

  private

  def require_delete_permission
    unless current_user.is_admin || record.owner == current_user
      flash[:error] = 'You do not have delete permissions'
      redirect_to somewhere
    end
  end
end
captaintokyo
Explain the downvote please! I don't see what's wrong with what I am suggesting. I would love to learn from you, dear downvoter.
captaintokyo
I also don't get the downvote here. Your suggestion is effectively what the accepted answer says.
d11wtq
A: 

I have come across the same issue in Rails 2.3 and finally come up with this solution. In your model you define some atribute, depending on which you switch on/off validation. Than you your control you set this attribute depending on the date available to controller (such as user privileges in your case) as follows:

Class Model < ActiveRecord::Base
   attr_accessor :perform_validation_of_field1 #This is an attribute which controller will use to turn on/off some validation logic depending on the current user

   validates_presence_of :field1, :if => :perform_validation_of_field1
   #This validation (or any similar one) will occur only if controller sets model.perform_validation_of_field1 to true.
end

Class MyController < ActionController::Base
   def update
     @item = Model.find(params[:id])
     @item.update_attribute(params[:item])

     #The controller decides whether to turn on optional validations depending on current user privileges (without the knowledge of internal implementation of this validation logic)
     @item.perform_validation_of_field1 = true unless active_user.admin?

     if @item.save
        flash[:success] = 'The record has been saved'
        redirect_to ...
     else
        flash.now[:error] = 'The record has not passed validation checks'
        render :action => :edit
     end
   end

I think that in Rails 3 it can be done in similar manner.

cryo28
I have to say, this sounds a bit haphazard on first impressions, but if it's working for you then great ;)
d11wtq
What's wrong with this approach. There is nothing about haphazard (sorry don't know how to make adjective in English from this word :). Yes I typed a code but didn't check it. I hoped that you would get the idea. The idea is to use :if or :unless parameters on validation macros defined in a model. So some parts of validation logic can be turned on/off. The controller layer knows how to enable/disable a particular validation logic for a model, but knows nothing about validation logic implementation.
cryo28
Of course you can combine validation options into a bigger chunks (there is no need to define one attribute for every validation).So please take a look at http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html and focus on :if and :unless parameters of validation macros in particular before deciding that this code works for me only as a coincidence :)
cryo28
Also I'd like to point out the my recipe is a general answer to your question - how to control some parts of validations from outside the model. In a particular case with deleted column I like Justice's answer.
cryo28
"There's nothing haphazard about it", just like we say "There's nothing strange about it", or "Is there something about about this?" ;)
d11wtq
(Sorry, I didn't realise ENTER submits the comment!) Ok, I guess I don't like conditional logic through switches/flags like this and would rather attach some sort of validation observer from the outside, if that's actually possible. That would offer a little more flexibility with what the validation is doing too. There's nothing wrong with your code, I'm just not a fan of transient/temporary variables used as switches to modify behaviour.
d11wtq