Rails 简单的访问控制

Rails simple access control

我知道有几个 gem 用于处理 Rails 中的授权。但是,将这些 gem 用于简单的访问控制真的值得吗?

我的应用里只有几个"roles",感觉再强大的gem也没什么用,甚至会拖慢响应时间

我已经实施了一个解决方案,但后来我采取了一些安全措施 类 (:p),我意识到我的模型是错误的("Allow by default, then restrict" 而不是 "Deny by default, then allow")。

现在我怎样才能简单地实现一个 "deny by default, allow on specific cases"?

基本上我想放在我的 ApplicationController 的最顶部

class ApplicationController < ApplicationController::Base
  before_filter :deny_access

在我其他控制器的最顶端:

class some_controller < ApplicationController
  before_filter :allow_access_to_[entity/user]

这些 allow_access_to_ before_filters 应该像 skip_before_filter

def allow_access_to_[...]
  skip_before_filter(:deny_access) if condition
end

但这不起作用,因为这些 allow_access 之前的过滤器不会在 deny_access before_filter

之前评估

对于访问控制的自定义实现,是否有更好的解决方案?

编辑

before_action :find_project, except: [:index, :new, :create]
before_action(except: [:show, :index, :new, :create]) do |c|
   c.restrict_access_to_manager(@project.manager)
end

我真的建议使用经过实战测试的 gem 进行身份验证和授权,而不是自己动手。这些 gem 有大量的测试套件,而且设置起来并不难。

我最近使用具有 Pundit & Devise

的角色实现了基于操作的授权

如果您不想进一步配置专家,只要您使用的 gem 提供 current_user 方法,设计就可以更改。

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include Pundit

  rescue_from Pundit::NotAuthorizedError, with: :rescue_unauthorized

  # Lock actions untill authorization is performed
  before_action :authorize_user

  # Fallback when not authorized
  def rescue_unauthorized(exception)
    policy_name = exception.policy.class.to_s.underscore
    flash[:notice] = t(
      "#{policy_name}.#{exception.query}",
      scope: "pundit",
      default: :default
    )
    redirect_to(request.referrer || root_path)
  end
end

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :roles, through: :memberships

  def authorized?(action)
    claim = String(action)
    roles.pluck(:claim).any? { |role_claim| role_claim == claim }
  end
end

# app/policies/user_policy.rb => maps to user_controller#actions
class UserPolicy < ApplicationPolicy
  class Scope < Scope
    attr_reader :user, :scope

    # user is automagically set to current_user
    def initialize(user, scope)
      @user = user
      @scope = scope
    end

    def resolve
      scope.all
    end
  end

  def index?
    # If user has a role which has the claim :view_users
    # Allow this user to use the user#index action
    @user.authorized? :view_users 
  end

  def new?
    @user.authorized? :new_users
  end

  def edit?
    @user.authorized? :edit_users
  end

  def create?
    new?
  end

  def update?
    edit?
  end

  def destroy?
    @user.authorized? :destroy_users
  end
end

长话短说:

如果您将 pundit 配置为对 github 页面中详细描述的每个请求强制授权,则控制器会根据使用的控制器评估策略。

UserController -> UserPolicy

动作用问号定义,即使是非 restful 路线。

def index?
  # authorization is done inside the method.
  # true = authorization succes
  # false = authorization failure
end

这是我基于操作的授权的解决方案希望它能帮到你。

欢迎优化和反馈!

编辑:过时的答案,我有一个更友好的实现,涉及使用 access_control

按照 evanbikes 的建议,现在我将使用 prepend_before 动作。我发现它非常简单和灵活,但如果我意识到它不够好,我会尝试其他的东西。

此外,如果您发现以下解决方案存在安全 issues/other 问题,请评论 and/or 投反对票。我不喜欢在 SO 中留下不好的例子。

class ApplicationController < ApplicationController::Base
  include AccessControl
  before_filter :access_denied
  ...

我的访问控制模块

module AccessControl
  extend ActiveSupport::Concern
  included do
    def access_denied(message: nil)
        unless @authorized
            flash.alert = 'Unauthorized access'
            flash.info = "Authorized entities : #{@authorized_entities.join(', '}" if @authorized_entities
            render 'static_pages/home', :status => :unauthorized
            end
        end

        def allow_access_to_managers
            (@authorized_entities ||= []) << "Project managers"
            @authorized = true if manager_logged_in?
        end
        ...

我如何在控制器中使用空调:

class ProjectController < ApplicationController
  # In reverse because `prepend_` is LIFO
  prepend_before_action(except: [:show, :index, :new, :create]) do |c|
    c.allow_access_to_manager(@manager.administrateur)
  end
  prepend_before_action :find_manager, except: [:index, :new, :create]

滚动你自己的实现不一定是坏事,只要你致力于它。

它不会得到社区的测试和维护,因此您必须愿意长期自己维护它 运行,如果它危及安全性,您需要真正确定自己在做什么做并格外小心。如果您已经涵盖了这些并且现有的替代方案并不能真正满足您的需求,那么自己制作并不是一个坏主意。总的来说,这是一次非常好的学习经历。

我用 ActionAccess 推出了自己的产品,我对结果非常满意。

  • 默认锁定 方法:

    class ApplicationController < ActionController::Base
      lock_access
    
      # ...
    end
    
  • 每次操作访问控制:

    class ArticlesController < ApplicationController
      let :admins, :all
      let :editors, [:index, :show, :edit, :update]
      let :all, [:index, :show]
    
      def index
        # ...
      end
    
      # ...
    end
    
  • 真正轻量级实现。

我建议你不要使用它,而是查看源代码,它有很多评论,应该是一个很好的灵感来源。 ControllerAdditions 可能是一个不错的起点。

ActionAccess 在内部采用不同的方法,但您可以重构您的答案以模仿它的 API,方法如下:

module AccessControl
  extend ActiveSupport::Concern

  included do
    before_filter :lock_access
  end

  module ClassMethods
    def lock_access
      unless @authorized
        # Redirect user...
      end
    end

    def allow_manager_to(actions = [])
      prepend_before_action only: actions do
        @authorized = true if current_user_is_a_manager?
      end
    end
  end
end

class ApplicationController < ActionController::Base
  include AccessControl  # Locked by default

  # ...
end

class ProjectController < ApplicationController
  allow_managers_to [:edit, :update]  # Per-action access control

  # ...
end

把这个例子当作伪代码,我没有测试过

希望这对您有所帮助。

我不喜欢我以前使用 prepend_before_action 的解决方案,这里是使用 ActionController 回调的一个很好的实现

module AccessControl
  extend ActiveSupport::Concern

  class UnauthorizedException < Exception
  end

  class_methods do
    define_method :access_control do |*names, &blk|
      _insert_callbacks(names, blk) do |name, options|
        set_callback(:access_control, :before, name, options)
      end
    end
  end

  included do

    define_callbacks :access_control

    before_action :deny_by_default
    around_action :perform_if_access_granted

    def perform_if_access_granted
      run_callbacks :access_control do
        if @access_denied and not @access_authorized
          @request_authentication = true unless user_signed_in?
          render(
            file: File.join(Rails.root, 'app/views/errors/403.html'),
            status: 403,
            layout: 'error')
        else
          yield
        end
      end
    end

    def deny_by_default
      @access_denied ||= true
    end

    def allow_access
      @access_authorized = true
    end
  end
end

然后您可以添加自己的 allow_access_to_x 方法(例如在同一个 AccessControl 关注点中):

def allow_access_to_participants_of(project)
  return unless user_signed_in?
  allow_access if current_user.in?(project.executants)
end

按以下方式在您的控制器中使用它(不要忘记在您的 ApplicationController

中包含 AccessControl
class ProjectsController < ApplicationController
  access_control(only: [:show, :edit, :update]) do
    set_project
    allow_access_to_participants_of(@project)
    allow_access_to_project_managers
  end

  def index; ...; end;
  def show; ...; end;
  def edit; ...; end;
  def update; ...; end;

  def set_project
    @project = Project.find(params[:project_id])
  end
end