Module: RolePlays::Mixin
- Defined in:
- lib/role_plays/mixin.rb
Overview
Declarative, role based authorization DSL.
class OrderPolicy
include RolePlays::Mixin
context :user, :order, :order_relation
role :user do
action :create, -> { true }
action :destroy do
order.user_id == user.id
end
action :edit do
order.user_id == user.id && order.completed?
end
permitted_attributes %i[title description]
permitted_attributes :list, %i[page per_page]
permitted_attributes(:create) do
%i[title description] + (order ? [:user_id] : [])
end
scope :list, -> { order_relation.where(sent: true) }
scope -> { order_relation.where(user_id: user.id) }
end
# Roles listed together share the block, each one getting the same declarations
role %i[user access_code] do
action :list, -> { true }
scope -> { order_relation.where(user_id: user.id) }
end
role :admin do
end
role :any do
action :list, -> { true }
end
end
policy = OrderPolicy.new(role: :user, user: current_user, order: order,
order_relation: Order.completed)
policy.can?(:destroy) # => true / false
policy.permitted_attributes(:list) # => %i[page per_page]
policy.scope(:list) # => Order.completed.where(sent: true)
Every declared action also gets a can_<action>? predicate — can_create?, can_destroy? — so
a policy can replace a hand written one without touching its callers.
role: is the only argument the policy asks for. Every other keyword is arbitrary: it is kept as
context and answered as a reader, so a policy is given what it actually talks about — user,
order, order_relation — rather than a fixed record/relation/options triple. context
declares those names, which makes them optional: a declared name reads as nil when the caller
leaves it out, while an undeclared one raises, so a typo in a handler is not read as nil.
The role is a symbol such as :user or :provider_location, selected from the authenticated user by the caller — the policy is told which role it answers for instead of resolving it from a user itself, so the same rules answer for a request, a background job or a spec.
Roles are stored per class as RolePlays::Mixin::Role structs. An action is looked up on the current role first and falls back to the :any role, so shared permissions can be declared once. Unknown role/action pairs are denied. Nothing is inherited: a policy declares the roles it answers for, and a role shared between policies is passed around as a Role struct built with RoleBuilder.
Permitted attributes and scopes are declared the same way, keyed by a label (:default when
omitted) and resolved with the same role/:any fallback. An unknown attributes label yields an
empty list, an unknown scope label yields nil — narrowing a relation to nothing is left to the
caller, the only side that knows which relation the policy was built around.
A policy is built once and never mutated afterwards. Action, attribute and scope bodies take no
arguments and are evaluated against the policy instance, which gives them access to role, to
every context keyword and to any helper method defined on the policy class itself.
The DSL is one module on purpose — the declarations, the lookups and the readers they feed are
one contract, so it is read top to bottom rather than split across files.
Defined Under Namespace
Modules: ClassMethods, ContextReaders Classes: Role, RoleBuilder
Constant Summary collapse
- ANY_ROLE =
Role every other role falls back to for actions it does not declare itself.
:any- DEFAULT_ATTRIBUTES_LABEL =
Label a
permitted_attributesdeclaration is filed under when none is given. :default- DEFAULT_SCOPE_LABEL =
Label a
scopedeclaration is filed under when none is given. :default
Instance Attribute Summary collapse
-
#context ⇒ Object
readonly
Returns the value of attribute context.
-
#role ⇒ Object
readonly
Returns the value of attribute role.
Class Method Summary collapse
Instance Method Summary collapse
- #can?(action) ⇒ Boolean
- #cannot?(action) ⇒ Boolean
-
#initialize(role:, **context) ⇒ Object
The role is the name a
roledeclaration was filed under,nilstanding for no role at all, which leaves only the :any declarations. -
#permitted_attributes(label = DEFAULT_ATTRIBUTES_LABEL) ⇒ Object
Attributes the current role may submit for the given label, always as an array ready to be handed to ActionController::Parameters#permit.
-
#scope(label = DEFAULT_SCOPE_LABEL) ⇒ Object
The relation narrowed to what the current role may see under the given label, or
nilwhen neither that role nor :any declares the label.
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(name, *args) ⇒ Object (private)
Reads a context keyword — new(role: :user, order:) answers order — so handlers can name
what they need. An unknown name still raises NameError, so a typo is not read as nil.
364 365 366 367 368 |
# File 'lib/role_plays/mixin.rb', line 364 def method_missing(name, *args) return super unless args.empty? && context.key?(name) context[name] end |
Instance Attribute Details
#context ⇒ Object (readonly)
Returns the value of attribute context.
314 315 316 |
# File 'lib/role_plays/mixin.rb', line 314 def context @context end |
#role ⇒ Object (readonly)
Returns the value of attribute role.
314 315 316 |
# File 'lib/role_plays/mixin.rb', line 314 def role @role end |
Class Method Details
.included(base) ⇒ Object
196 197 198 |
# File 'lib/role_plays/mixin.rb', line 196 def self.included(base) base.extend(ClassMethods) end |
Instance Method Details
#can?(action) ⇒ Boolean
328 329 330 331 332 333 |
# File 'lib/role_plays/mixin.rb', line 328 def can?(action) handler = self.class.action_handler(role, action.to_sym) return false if handler.nil? !!instance_exec(&handler) end |
#cannot?(action) ⇒ Boolean
335 336 337 |
# File 'lib/role_plays/mixin.rb', line 335 def cannot?(action) !can?(action) end |
#initialize(role:, **context) ⇒ Object
The role is the name a role declaration was filed under, nil standing for no role at all,
which leaves only the :any declarations. It is the only argument the policy asks for — every
other keyword is kept as context and read back by name:
OrderPolicy.new(role: :user, user: current_user, order: order, order_relation: Order.all)
321 322 323 324 325 326 |
# File 'lib/role_plays/mixin.rb', line 321 def initialize(role:, **context) reject_shadowing_context!(context) @role = role&.to_sym @context = context.freeze end |
#permitted_attributes(label = DEFAULT_ATTRIBUTES_LABEL) ⇒ Object
Attributes the current role may submit for the given label, always as an array ready to be handed to ActionController::Parameters#permit
341 342 343 344 345 346 |
# File 'lib/role_plays/mixin.rb', line 341 def permitted_attributes(label = DEFAULT_ATTRIBUTES_LABEL) handler = self.class.permitted_attributes_handler(role, label.to_sym) return [] if handler.nil? wrap_attributes(instance_exec(&handler)) end |
#scope(label = DEFAULT_SCOPE_LABEL) ⇒ Object
The relation narrowed to what the current role may see under the given label, or nil when
neither that role nor :any declares the label. The policy carries its relation in a keyword of
its own naming, so saying "nothing is visible" is left to the caller:
OrderPolicy.new(role:, order_relation: Order.all).scope(:list) || Order.none
353 354 355 356 357 358 |
# File 'lib/role_plays/mixin.rb', line 353 def scope(label = DEFAULT_SCOPE_LABEL) handler = self.class.scope_handler(role, label.to_sym) return nil if handler.nil? instance_exec(&handler) end |