使用 declarative_authorization 保护敏感属性

发布于 2024-10-21 08:44:53 字数 445 浏览 6 评论 0原文

使用 declarative_authorization 按角色保护属性的好方法是什么?例如,用户可以编辑他的联系信息,但不能编辑他的角色。

我的第一个倾向是为不同的场景创建多个控制器操作。我很快意识到,随着受保护属性数量的增加,这会变得多么笨拙。为用户角色执行此操作是一回事,但我可以想象多个受保护的属性。添加大量控制器操作和路线感觉不太对。

我的第二个倾向是围绕特定的敏感属性创建权限,然后使用 declarative_authorizations 提供的 View hepers 包装表单元素。然而,模型和控制器方面在我看来有点模糊。建议会很棒。

请建议使用 declarative_authorizations 按角色保护属性的最佳方法。

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

末蓝 2024-10-28 08:44:54

编辑2011-05-22
从 3.1RC 开始,Rails 中现在也有类似的东西 https:/ /github.com/rails/rails/blob/master/activerecord/test/cases/mass_assignment_security_test.rb 所以我建议现在就走这条路。

原始答案
我只需将之前使用的内容移植到 Rails 3 中。我从未专门使用过声明性授权,但这非常简单明了,您应该能够适应它。

Rails 3 添加了 mass_assignment_authorizer,这使得这一切真正实现简单的。我使用该链接的教程作为基础,并通过类继承并将属性分组为角色,使其更好地适合我的域模型。

在模型中

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

在控制器中

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

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
找回味觉 2024-10-28 08:44:54

对我来说,这个过滤问题应该在控制器级别应用。

您需要在某个地方定义如何决定哪些属性对于给定用户是可写的。

# On the user model
class User < ActiveRecord::Base
  # ...

  # Return a list of symbols representing the accessible attributes
  def self.allowed_params(user)
    if user.admin?
      [:name, :email, :role]
    else
      [:name, email]
    end
  end
end

然后,在应用程序控制器中,您可以定义一个方法来过滤参数。

class ApplicationController < ActionController::Base
  # ...
  protected

  def restrict_params(param, model, user)
    params[param].reject! do |k,v|
      !model.allowed_params(user).include?(k)
    end
  end
  # ...
end

最后,在控制器操作中,您可以使用此过滤器:

class UserController < ActionController::Base
  # ...
  def update
    restrict_params(:user, User, @current_user)
    # and continue as normal
  end
  # ...
end

想法是,您可以在每个模型上定义 allowed_pa​​rams,并让每个模型的控制器使用相同的过滤器方法。您可以通过在应用程序控制器中使用一个方法来保存一些样板文件,该方法可以发出前置过滤器,如下所示:

def self.param_restrictions(param, model)
  before_filter do
    restrict_params(param, model, @current_user) if params[param]
  end
end

# in UserController
param_restrictions :user, User

这些示例旨在说明性而非确定性,我希望它们有助于实现此目的。

To me this filtering problem is something that should be applied at the controller level.

You'll want to have something somewhere that defines how to decide which attributes are writeable for a given user.

# On the user model
class User < ActiveRecord::Base
  # ...

  # Return a list of symbols representing the accessible attributes
  def self.allowed_params(user)
    if user.admin?
      [:name, :email, :role]
    else
      [:name, email]
    end
  end
end

Then, in the application controller you can define a method to filter parameters.

class ApplicationController < ActionController::Base
  # ...
  protected

  def restrict_params(param, model, user)
    params[param].reject! do |k,v|
      !model.allowed_params(user).include?(k)
    end
  end
  # ...
end

And finally in your controller action you can use this filter:

class UserController < ActionController::Base
  # ...
  def update
    restrict_params(:user, User, @current_user)
    # and continue as normal
  end
  # ...
end

The idea is that you could then define allowed_params on each of your models, and have the controllers for each of these use the same filter method. You could save some boilerplate by having a method in application controller that spits out a before filter, like this:

def self.param_restrictions(param, model)
  before_filter do
    restrict_params(param, model, @current_user) if params[param]
  end
end

# in UserController
param_restrictions :user, User

These examples are intended to be illustrative rather than definitive, I hope they help with the implementation of this.

栩栩如生 2024-10-28 08:44:54

我会使用 scoped_attr_accessible ,它看起来正是您正在寻找的。只需在所有模型的请求开始时设置范围即可。

为此,请在 application_controller.rb 中使用 before_filter

before_filter do |controller|
  ScopedAttrAccessible.current_sanitizer_scope = controller.current_user.role
end

I'd use scoped_attr_accessible, which looks like just what you're looking for. Only you need to set the scope at the start of a request for all models.

To do that, use a before_filter in your application_controller.rb:

before_filter do |controller|
  ScopedAttrAccessible.current_sanitizer_scope = controller.current_user.role
end
江心雾 2024-10-28 08:44:54

我会避免基于模型中用户访问的每个解决方案,因为它似乎存在潜在危险。我会尝试这种方法:

class User < ActiveRecord::Base

  def update_attributes_as_user(values, user)
    values.each do |attribute, value|
      # Update the attribute if the user is allowed to
      @user.send("#{attribute}=", value) if user.modifiable_attributes.include?(attribute)
    end
    save
  end

  def modifiable_attributes
    admin? ? [:name, :email, :role] : [:name, :email]
  end
end

然后在您的控制器中将更新操作从:

@user.update_attributes(params[:user])

更改为

@user.update_attributes_as_user(params[:user], current_user)

I would avoid every solution based on user access in model because it seems potentially dangerous. I would try this approach:

class User < ActiveRecord::Base

  def update_attributes_as_user(values, user)
    values.each do |attribute, value|
      # Update the attribute if the user is allowed to
      @user.send("#{attribute}=", value) if user.modifiable_attributes.include?(attribute)
    end
    save
  end

  def modifiable_attributes
    admin? ? [:name, :email, :role] : [:name, :email]
  end
end

Then in your controller change your update action from:

@user.update_attributes(params[:user])

to

@user.update_attributes_as_user(params[:user], current_user)
初吻给了烟 2024-10-28 08:44:54

Rails 3.1+ 为此目的提供了一个 +assign_attributes+ 方法 - http://apidock.com/rails/ ActiveRecord/AttributeAssignment/assign_attributes

Rails 3.1+ comes with a +assign_attributes+ method for this purpose - http://apidock.com/rails/ActiveRecord/AttributeAssignment/assign_attributes.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文