Operation Policy
Last updated 29 August 2017 trailblazer v2.0 v1.1This document discusses the Policy
module, Policy::Pundit
, and Policy::Guard
.
Pundit
The Policy::Pundit
module allows using Pundit-compatible policy classes in an operation.
A Pundit policy has various rule methods and a special constructor that receives the current user and the current model.
class MyPolicy
def initialize(user, model)
@user, @model = user, model
end
def create?
@user == Module && @model.id.nil?
end
def new?
@user == Class
end
end
In pundit policies, it is a convention to have access to those objects at runtime and build rules on top of those.
You can plug this policy into your pipe at any point. However, this must be inserted after the "model"
skill is available.
class Create < Trailblazer::Operation
step Model( Song, :new )
step Policy::Pundit( MyPolicy, :create? )
# ...
end
Note that you don’t have to create the model via the Model
macro - you can use any logic you want. The Pundit
macro will grab the model from ["model"]
, though.
This policy will only pass when the operation is invoked as follows.
Create.( {}, "current_user" => User.find(1) )
Any other call will cause a policy breach and stop the pipe from executing after the Policy::Pundit
step.
Pundit: API
Add your polices using the Policy::Pundit
macro. It accepts the policy class name, and the rule method to call.
class Create < Trailblazer::Operation
step Model( Song, :new )
step Policy::Pundit( MyPolicy, :create? )
# ...
end
The step will create the policy instance automatically for you and passes the "model"
and the "current_user"
skill into the policies constructor. Just make sure those dependencies are available before the step is executed.
If the policy returns falsey
, it deviates to the left track.
After running the Pundit
step, its result is readable from the Result
object.
result = Create.({}, "current_user" => Module)
result["result.policy.default"].success? #=> true
result["result.policy.default"]["policy"] #=> #<MyPolicy ...>
Note that the actual policy instance is available via ["result.policy.#{name}"]["policy"]
to be reinvoked with other rules (e.g. in the view layer).
Pundit: Name
You can add any number of Pundit policies to your pipe. Make sure to use name:
to name them, though.
class Create < Trailblazer::Operation
step Model( Song, :new )
step Policy::Pundit( MyPolicy, :create?, name: "after_model" )
# ...
end
The result will be stored in "result.policy.#{name}"
result = Create.({}, "current_user" => Module)
result["result.policy.after_model"].success? #=> true
Pundit: Dependency Injection
Override a configured policy using dependency injection.
Create.({},
"current_user" => Module,
"policy.default.eval" => Trailblazer::Operation::Policy::Pundit.build(AnotherPolicy, :create?)
)
You can inject it using "policy.#{name}.eval"
. It can be any object responding to call
.
Guard
A guard is a step that helps you evaluating a condition and writing the result. If the condition was evaluated as falsey
, the pipe won’t be further processed and a policy breach is reported in Result["result.policy.default"]
.
class Create < Trailblazer::Operation
step Policy::Guard( ->(options, params:, **) { params[:pass] } )
step :process
def process(*)
self["x"] = true
end
end
The only way to make the above operation invoke the second step :process
is as follows.
result = Create.({ pass: true })
result["x"] #=> true
Any other input will result in an abortion of the pipe after the guard.
result = Create.()
result["x"] #=> nil
result["result.policy.default"].success? #=> false
Learn more about → dependency injection to pass params and current user into the operation.
Guard: API
The Policy::Guard
macro helps you inserting your guard logic. If not defined, it will be evaluated where you insert it.
class Create < Trailblazer::Operation
step Policy::Guard( ->(options, params:, **) { params[:pass] } )
# ...
end
The options
object is passed into the guard and allows you to read and inspect data like params
or current_user
. Please use kw args.
Guard: Callable
As always, the guard can also be a Callable
-marked object.
class MyGuard
include Uber::Callable
def call(options, params:, **)
params[:pass]
end
end
Insert the object instance via the Policy::Guard
macro.
class Create < Trailblazer::Operation
step Policy::Guard( MyGuard.new )
# ...
end
Guard: Instance Method
As always, you may also use an instance method to implement a guard.
class Create < Trailblazer::Operation
step Policy::Guard( :pass? )
def pass?(options, params:, **)
params[:pass]
end
# ...
end
Guard: Name
The guard name defaults to default
and can be set via name:
. This allows having multiple guards.
class Create < Trailblazer::Operation
step Policy::Guard( ->(options, current_user:, **) { current_user }, name: :user )
# ...
end
The result will sit in result.policy.#{name}
.
result = Create.({}, "current_user" => true)
result["result.policy.user"].success? #=> true
Guard: Dependency Injection
Instead of using the configured guard, you can inject any callable object that returns a Result
object. Do so by overriding the policy.#{name}.eval
path when calling the operation.
Create.({},
"current_user" => Module,
"policy.default.eval" => Trailblazer::Operation::Policy::Guard.build(->(options) { false })
)
An easy way to let Trailblazer build a compatible object for you is using Guard.build
.
This is helpful to override a certain policy for testing, or to invoke it with special rights, e.g. for an admin.
Guard: Position
You may specify a position.
class Create < Trailblazer::Operation
step :model!
step Policy::Guard( :authorize! ),
before: :model!
end
Resulting in the guard inserted before model!
, even though it was added at a later point.
puts Create["pipetree"].inspect(style: :rows) #=>
# 0 ========================>operation.new
# 1 ==================>policy.default.eval
# 2 ===============================>model!
This is helpful if you maintain modules for operations with generic steps.