ruby-on-railsrubyauthorizationdeclarative-authorization

Protect sensitive attributes with declarative_authorization


What's a cool way to protect attributes by role using declarative_authorization? For example, a user can edit his contact information but not his role.

My first inclination was to create multiple controller actions for different scenarios. I quickly realized how unwieldy this could become as the number of protected attributes grows. Doing this for user role is one thing, but I can imagine multiple protected attributes. Adding a lot controller actions and routes doesn't feel right.

My second inclination was to create permissions around specific sensitive attributes and then wrap the form elements with View hepers provided by declarative_authorizations. However, the model and controller aspect of this is a bit foggy in my mind. Suggestions would be awesome.

Please advise on the best way to protect attributes by role using declarative_authorizations.


Solution

  • EDIT 2011-05-22
    Something similar is now in Rails as of 3.1RC https://github.com/rails/rails/blob/master/activerecord/test/cases/mass_assignment_security_test.rb so I would suggest going that route now.

    ORIGINAL ANSWER
    I just had to port what I had been using previously to Rails 3. I've never used declarative authorization specifically, but this is pretty simple and straightforward enough that you should be able to adapt to it.

    Rails 3 added mass_assignment_authorizer, which makes this all really simple. I used that linked tutorial as a basis and just made it fit my domain model better, with class inheritance and grouping the attributes into roles.

    In model

    acts_as_accessible :admin => :all, :moderator => [:is_spam, :is_featured]
    attr_accessible :title, :body # :admin, :moderator, and anyone else can set these
    

    In controller

    post.accessed_by(current_user.roles.collect(&:code)) # or however yours works
    post.attributes = params[:post]
    

    lib/active_record/acts_as_accessible.rb

    # A way to have different attr_accessible attributes based on a Role
    # @see ActsAsAccessible::ActMethods#acts_as_accessible
    module ActiveRecord
      module ActsAsAccessible
        module ActMethods
          # In model
          # acts_as_accessible :admin => :all, :moderator => [:is_spam]
          # attr_accessible :title, :body
          #
          # In controller
          # post.accessed_by(current_user.roles.collect(&:code))
          # post.attributes = params[:post]
          #
          # Warning: This frequently wouldn't be the concern of the model where this is declared in,
          # but it is so much more useful to have it in there with the attr_accessible declaration.
          # OHWELL.
          #
          # @param [Hash] roles Hash of { :role => [:attr, :attr] }
          # @see acts_as_accessible_attributes
          def acts_as_accessible(*roles)
            roles_attributes_hash = Hash.new {|h,k| h[k] ||= [] }
            roles_attributes_hash = roles_attributes_hash.merge(roles.extract_options!).symbolize_keys
    
            if !self.respond_to? :acts_as_accessible_attributes
              attr_accessible
              write_inheritable_attribute :acts_as_accessible_attributes, roles_attributes_hash.symbolize_keys
              class_inheritable_reader    :acts_as_accessible_attributes
    
              # extend ClassMethods unless (class << self; included_modules; end).include?(ClassMethods)
              include InstanceMethods unless included_modules.include?(InstanceMethods)
            else # subclass
              new_acts_as_accessible_attributes = self.acts_as_accessible_attributes.dup
              roles_attributes_hash.each do |role,attrs|
                new_acts_as_accessible_attributes[role] += attrs
              end
              write_inheritable_attribute :acts_as_accessible_attributes, new_acts_as_accessible_attributes.symbolize_keys
            end
          end
        end
    
        module InstanceMethods
          # @param [Array, NilClass] roles Array of Roles or nil to reset
          # @return [Array, NilClass]
          def accessed_by(*roles)
            if roles.any?
              case roles.first
              when NilClass
                @accessed_by = nil
              when Array
                @accessed_by = roles.first.flatten.collect(&:to_sym)
              else
                @accessed_by = roles.flatten.flatten.collect(&:to_sym)
              end
            end
            @accessed_by
          end
    
          private
          # This is what really does the work in attr_accessible/attr_protected.
          # This override adds the acts_as_accessible_attributes for the current accessed_by roles.
          # @see http://asciicasts.com/episodes/237-dynamic-attr-accessible
          def mass_assignment_authorizer
            attrs = []
            if self.accessed_by
              self.accessed_by.each do |role|
                if self.acts_as_accessible_attributes.include? role
                  if self.acts_as_accessible_attributes[role] == :all
                    return self.class.protected_attributes
                  else
                    attrs += self.acts_as_accessible_attributes[role]
                  end
                end
              end
            end
            super + attrs
          end
        end
      end
    end
    
    ActiveRecord::Base.send(:extend, ActiveRecord::ActsAsAccessible::ActMethods)
    

    spec/lib/active_record/acts_as_accessible.rb

    require 'spec_helper'
    
    class TestActsAsAccessible
      include ActiveModel::MassAssignmentSecurity
      extend ActiveRecord::ActsAsAccessible::ActMethods
      attr_accessor :foo, :bar, :baz, :qux
      acts_as_accessible :dude => [:bar], :bra => [:baz, :qux], :admin => :all
      attr_accessible :foo
      def attributes=(values)
        sanitize_for_mass_assignment(values).each do |k, v|
          send("#{k}=", v)
        end
      end
    end
    
    describe TestActsAsAccessible do
      it "should still allow mass assignment to accessible attributes by default" do
        subject.attributes = {:foo => 'fooo'}
        subject.foo.should == 'fooo'
      end
      it "should not allow mass assignment to non-accessible attributes by default" do
        subject.attributes = {:bar => 'baaar'}
        subject.bar.should be_nil
      end
      it "should allow mass assignment to acts_as_accessible attributes when passed appropriate accessed_by" do
        subject.accessed_by :dude
        subject.attributes = {:bar => 'baaar'}
        subject.bar.should == 'baaar'
      end
      it "should allow mass assignment to multiple acts_as_accessible attributes when passed appropriate accessed_by" do
        subject.accessed_by :bra
        subject.attributes = {:baz => 'baaaz', :qux => 'quuux'}
        subject.baz.should == 'baaaz'
        subject.qux.should == 'quuux'
      end
      it "should allow multiple accessed_by to be specified" do
        subject.accessed_by :dude, :bra
        subject.attributes = {:bar => 'baaar', :baz => 'baaaz', :qux => 'quuux'}
        subject.bar.should == 'baaar'
        subject.baz.should == 'baaaz'
        subject.qux.should == 'quuux'
      end
      it "should allow :all access" do
        subject.accessed_by :admin
        subject.attributes = {:bar => 'baaar', :baz => 'baaaz', :qux => 'quuux'}
        subject.bar.should == 'baaar'
        subject.baz.should == 'baaaz'
        subject.qux.should == 'quuux'
      end
    end