我知道有几个宝石可以在Rails中处理授权。但是使用这些宝石进行简单的访问控制真的值得吗?
我的应用程序中只有一些“角色”,我觉得强大的宝石会毫无用处,甚至会缩短响应时间。
我已经实现了一个解决方案,但后来我接受了一些安全类(:p)并且我意识到我的模型是错误的(“默认允许,然后限制”而不是“默认拒绝,然后允许”)。< / p>
现在我怎样才能简单地实现“默认拒绝,允许特定情况”?
基本上我想放在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
before_filter
deny_access
任何解决方法,这种自定义访问控制实现的更好解决方案?
修改
undefined method 'skip_before_filter' for #<MyController...
为什么?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
答案 0 :(得分:3)
我真的建议使用经过适当战斗测试的宝石进行身份验证&amp;授权而不是滚动自己。这些宝石拥有巨大的测试套件,并且真的难以设置。
我最近使用Pundit&amp;的角色实施了基于操作的授权。 Devise
只要您使用的宝石提供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
使用问号定义操作,甚至是非静止路径。
def index?
# authorization is done inside the method.
# true = authorization succes
# false = authorization failure
end
这是我基于行动的授权解决方案希望它能帮到你。
优化&amp;欢迎提出反馈!
答案 1 :(得分:1)
只要您致力于推动自己的实施并不一定是坏事。
社区不会对其进行测试和维护,因此从长远来看,您必须愿意自己维护它,如果它危及安全性,您需要确定自己正在做的事情,并采取额外的谨慎措施。如果你已经覆盖了现有的替代品并不能满足你的需求,那么制作你自己的替代品并不是一个坏主意。一般来说,这是一次非常好的学习经历。
我用ActionAccess滚动自己,我对结果感到高兴。
默认锁定 aproach:
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
以伪代码为例,我没有测试过。
希望这有帮助。
答案 2 :(得分:0)
编辑:过时的答案,我有一个更友好的实施,涉及使用access_control
块
与evanbikes建议一起,现在我将使用prepend_before动作。我发现它非常简单&amp;灵活,但如果我意识到它不够好,我会尝试其他的东西。
此外,如果您发现以下解决方案存在安全问题/其他问题,请发表评论和/或关注。我不喜欢在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
...
我如何在控制器中使用AC:
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]
答案 3 :(得分:0)
我不喜欢使用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
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