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
之前评估
对于访问控制的自定义实现,是否有更好的解决方案?
编辑
- 许多非RESTful 操作
- 我需要按操作访问控制
undefined method 'skip_before_filter' for #<MyController...
为什么?
- 我的 before_filters 会变得棘手
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 有大量的测试套件,而且设置起来并不难。
的角色实现了基于操作的授权
如果您不想进一步配置专家,只要您使用的 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
我知道有几个 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
对于访问控制的自定义实现,是否有更好的解决方案?
编辑
- 许多非RESTful 操作
- 我需要按操作访问控制
undefined method 'skip_before_filter' for #<MyController...
为什么?- 我的 before_filters 会变得棘手
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 有大量的测试套件,而且设置起来并不难。
的角色实现了基于操作的授权如果您不想进一步配置专家,只要您使用的 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
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