By scoping we mean an ability to use policies to scope data (or filter/modify/transform/choose-your-verb).
The most common situation is when you want to scope ActiveRecord relations depending on the current user permissions. Without policies it could look like this:
class PostsController < ApplicationController
def index
@posts =
if current_user.admin?
Post.all
else
Post.where(user: current_user)
end
end
end
That's a very simplified example. In practice scoping rules might be more complex, and it's likely that we would use them in multiple places.
Action Policy allows you to define scoping rules within a policy class and use them with the help of authorized_scope
method (authorized
alias is also available):
class PostsController < ApplicationController
def index
@posts = authorized_scope(Post.all)
end
end
class PostPolicy < ApplicationPolicy
relation_scope do |relation|
next relation if user.admin?
relation.where(user: user)
end
end
To define scope you should use either scope_for
or smth_scope
methods in your policy:
class PostPolicy < ApplicationPolicy
# define a scope of a `relation` type
scope_for :relation do |relation|
relation.where(user: user)
end
# define a scope of `my_data` type,
# which acts on hashes
scope_for :my_data do |data|
next data if user.admin?
data.delete_if { |k, _| SENSITIVE_KEYS.include?(k) }
end
end
Scopes have types: different types of scopes are meant to be applied to different data types.
You can specify multiple scopes (named scopes) for the same type providing a scope name:
class EventPolicy < ApplictionPolicy
scope_for :relation, :own do |relation|
relation.where(owner: user)
end
end
When the second argument is not specified, the :default
is implied as the scope name.
Also, there are cases where it might be easier to add options to existing scope than create a new one.
For example, if you use soft-deletion and your logic inside a scope depends on if deleted records are included, you can add with_deleted
option:
class PostPolicy < ApplicationPolicy
scope_for :relation do |relation, with_deleted: false|
rel = some_logic(relation)
with_deleted ? rel.with_deleted : rel
end
end
You can add as many options as you want:
class PostPolicy < ApplicationPolicy
scope_for :relation do |relation, some_required_option:, with_deleted: false, magic_number: 42|
# Your code
end
end
Action Policy behaviour (ActionPolicy::Behaviour
) provides an authorized
method which allows you to use scoping:
class PostsController < ApplicationController
def index
# The first argument is the target,
# which is passed to the scope block
#
# The second argument is the scope type
@posts = authorized_scope(Post, type: :relation)
#
# For named scopes provide `as` option
@events = authorized_scope(Event, type: :relation, as: :own)
#
# If you want to specify scope options provide `scope_options` option
@events = authorized_scope(Event, type: :relation, scope_options: {with_deleted: true})
end
end
You can also specify additional options for policy class inference (see behaviour docs). For example, to explicitly specify the policy class use:
@posts = authorized_scope(Post, with: CustomPostPolicy)
You can also use scopes within policy classes using the same authorized_scope
method.
For example:
relation_scope(:edit) do |scope|
teachers = authorized_scope(Teacher.all, as: :edit)
scope
.joins(:teachers)
.where(teacher_id: teachers)
end
To use scopes without including Action Policy behaviour do the following:
# initialize policy
policy = ApplicantPolicy.new(user: user)
# apply scope
policy.apply_scope(User.all, type: :relation)
Action Policy could look up a scope type if it's not specified and if scope matchers were configured.
Scope matcher is an object that implements #===
(case equality) or a Proc. You can define it within a policy class:
class ApplicationPolicy < ActionPolicy::Base
scope_matcher :relation, ActiveRecord::Relation
# use Proc to handle AR models classes
scope_matcher :relation, ->(target) { target < ActiveRecord::Base }
scope_matcher :custom, MyCustomClass
end
Adding a scope matcher also adds a DSL to define scope rules (just a syntax sugar):
class ApplicationPolicy < ActionPolicy::Base
scope_matcher :relation, ActiveRecord::Relation
# now you can define scope rules like this
relation_scope { |relation| relation }
end
When authorized_scope
is called without the explicit scope type, Action Policy uses matchers (in the order they're defined) to infer the type.
Action Policy provides a couple of scope matchers out-of-the-box for Active Record relations and Action Controller parameters.
Scope type :relation
is automatically applied to the object of ActiveRecord::Relation
type.
To define Active Record scopes you can use relation_scope
macro (which is just an alias for scope :relation
) in your policy:
class PostPolicy < ApplicationPolicy
# Equals `scope_for :active_record_relation do ...`
relation_scope do |scope|
if super_user? || admin?
scope
else
scope.joins(:accesses).where(accesses: {user_id: user.id})
end
end
# define named scope
relation_scope(:own) do |scope|
next scope.none if user.guest?
scope.where(user: user)
end
end
NOTE: the :active_record_relation
scoping is used if and only if an ActiveRecord::Relation
is passed to authorized
:
def index
# BAD: Post is not a relation; raises an exception
@posts = authorized_scope(Post)
# GOOD:
@posts = authorized_scope(Post.all)
end
Use scopes of type :params
if your strong parameters filtering depends on the current user:
class UserPolicy < ApplicationPolicy
# Equals to `scope_for :action_controller_params do ...`
params_filter do |params|
if user.admin?
params.permit(:name, :email, :role)
else
params.permit(:name)
end
end
params_filter(:update) do |params|
params.permit(:name)
end
end
class UsersController < ApplicationController
def create
# Call `authorized_scope` on `params` object
@user = User.create!(authorized_scope(params.require(:user)))
# Or you can use `authorized` alias which fits this case better
@user = User.create!(authorized(params.require(:user)))
head :ok
end
def update
@user.update!(authorized_scope(params.require(:user), as: :update))
head :ok
end
end