action_policy 0.5.0 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -2
- data/config/rubocop-rspec.yml +17 -0
- data/lib/.rbnext/1995.next/action_policy/behaviours/policy_for.rb +62 -0
- data/lib/.rbnext/1995.next/action_policy/behaviours/scoping.rb +35 -0
- data/lib/.rbnext/1995.next/action_policy/policy/authorization.rb +87 -0
- data/lib/.rbnext/1995.next/action_policy/utils/pretty_print.rb +159 -0
- data/lib/.rbnext/2.7/action_policy/behaviours/policy_for.rb +62 -0
- data/lib/.rbnext/2.7/action_policy/i18n.rb +56 -0
- data/lib/.rbnext/2.7/action_policy/policy/cache.rb +101 -0
- data/lib/.rbnext/2.7/action_policy/policy/pre_check.rb +162 -0
- data/lib/.rbnext/2.7/action_policy/rspec/be_authorized_to.rb +89 -0
- data/lib/.rbnext/2.7/action_policy/rspec/have_authorized_scope.rb +124 -0
- data/lib/.rbnext/2.7/action_policy/utils/pretty_print.rb +159 -0
- data/lib/.rbnext/3.0/action_policy/behaviours/thread_memoized.rb +59 -0
- data/lib/.rbnext/3.0/action_policy/ext/policy_cache_key.rb +72 -0
- data/lib/.rbnext/3.0/action_policy/policy/aliases.rb +69 -0
- data/lib/.rbnext/3.0/action_policy/policy/cache.rb +101 -0
- data/lib/.rbnext/3.0/action_policy/policy/core.rb +161 -0
- data/lib/.rbnext/3.0/action_policy/policy/defaults.rb +31 -0
- data/lib/.rbnext/3.0/action_policy/policy/execution_result.rb +37 -0
- data/lib/.rbnext/3.0/action_policy/policy/pre_check.rb +162 -0
- data/lib/.rbnext/3.0/action_policy/policy/reasons.rb +212 -0
- data/lib/.rbnext/3.0/action_policy/rspec/be_authorized_to.rb +89 -0
- data/lib/.rbnext/3.0/action_policy/rspec/have_authorized_scope.rb +124 -0
- data/lib/.rbnext/3.0/action_policy/utils/pretty_print.rb +159 -0
- data/lib/.rbnext/3.0/action_policy/utils/suggest_message.rb +19 -0
- data/lib/action_policy/behaviour.rb +2 -2
- data/lib/action_policy/behaviours/memoized.rb +1 -1
- data/lib/action_policy/behaviours/namespaced.rb +1 -1
- data/lib/action_policy/behaviours/policy_for.rb +1 -1
- data/lib/action_policy/behaviours/scoping.rb +2 -2
- data/lib/action_policy/behaviours/thread_memoized.rb +1 -1
- data/lib/action_policy/lookup_chain.rb +11 -27
- data/lib/action_policy/policy/aliases.rb +2 -2
- data/lib/action_policy/policy/authorization.rb +2 -2
- data/lib/action_policy/policy/cache.rb +2 -2
- data/lib/action_policy/policy/pre_check.rb +2 -2
- data/lib/action_policy/policy/reasons.rb +4 -2
- data/lib/action_policy/policy/scoping.rb +2 -2
- data/lib/action_policy/test_helper.rb +1 -0
- data/lib/action_policy/version.rb +1 -1
- metadata +29 -4
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_policy/behaviours/policy_for"
|
4
|
+
require "action_policy/policy/execution_result"
|
5
|
+
require "action_policy/utils/suggest_message"
|
6
|
+
require "action_policy/utils/pretty_print"
|
7
|
+
|
8
|
+
unless "".respond_to?(:underscore)
|
9
|
+
require "action_policy/ext/string_underscore"
|
10
|
+
using ActionPolicy::Ext::StringUnderscore
|
11
|
+
end
|
12
|
+
|
13
|
+
module ActionPolicy
|
14
|
+
using RubyNext
|
15
|
+
|
16
|
+
# Raised when `resolve_rule` failed to find an approriate
|
17
|
+
# policy rule method for the activity
|
18
|
+
class UnknownRule < Error
|
19
|
+
include ActionPolicy::SuggestMessage
|
20
|
+
|
21
|
+
attr_reader :policy, :rule, :message
|
22
|
+
|
23
|
+
def initialize(policy, rule)
|
24
|
+
@policy = policy.class
|
25
|
+
@rule = rule
|
26
|
+
@message =
|
27
|
+
"Couldn't find rule '#{@rule}' for #{@policy}" \
|
28
|
+
"#{suggest(@rule, @policy.instance_methods - Object.instance_methods)}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module Policy
|
33
|
+
# Core policy API
|
34
|
+
module Core
|
35
|
+
class << self
|
36
|
+
def included(base)
|
37
|
+
base.extend ClassMethods
|
38
|
+
|
39
|
+
# Generate a new class for each _policy chain_
|
40
|
+
# in order to extend it independently
|
41
|
+
base.module_eval do
|
42
|
+
@result_class = Class.new(ExecutionResult)
|
43
|
+
|
44
|
+
# we need to make this class _named_,
|
45
|
+
# 'cause anonymous classes couldn't be marshalled
|
46
|
+
base.const_set(:APR, @result_class)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
module ClassMethods # :nodoc:
|
52
|
+
attr_writer :identifier
|
53
|
+
|
54
|
+
def result_class
|
55
|
+
return @result_class if instance_variable_defined?(:@result_class)
|
56
|
+
@result_class = superclass.result_class
|
57
|
+
end
|
58
|
+
|
59
|
+
def identifier
|
60
|
+
return @identifier if instance_variable_defined?(:@identifier)
|
61
|
+
|
62
|
+
@identifier = name.sub(/Policy$/, "").underscore.to_sym
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
include ActionPolicy::Behaviours::PolicyFor
|
67
|
+
|
68
|
+
attr_reader :record, :result
|
69
|
+
|
70
|
+
# NEXT_RELEASE: deprecate `record` arg, migrate to `record: nil`
|
71
|
+
def initialize(record = nil, *)
|
72
|
+
@record = record
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns a result of applying the specified rule (true of false).
|
76
|
+
# Unlike simply calling a predicate rule (`policy.manage?`),
|
77
|
+
# `apply` also calls pre-checks.
|
78
|
+
def apply(rule)
|
79
|
+
@result = self.class.result_class.new(self.class, rule)
|
80
|
+
|
81
|
+
catch :policy_fulfilled do
|
82
|
+
result.load __apply__(rule)
|
83
|
+
end
|
84
|
+
|
85
|
+
result.value
|
86
|
+
end
|
87
|
+
|
88
|
+
def deny!
|
89
|
+
result&.load false
|
90
|
+
throw :policy_fulfilled
|
91
|
+
end
|
92
|
+
|
93
|
+
def allow!
|
94
|
+
result&.load true
|
95
|
+
throw :policy_fulfilled
|
96
|
+
end
|
97
|
+
|
98
|
+
# This method performs the rule call.
|
99
|
+
# Override or extend it to provide custom functionality
|
100
|
+
# (such as caching, pre checks, etc.)
|
101
|
+
def __apply__(rule) ; public_send(rule); end
|
102
|
+
|
103
|
+
# Wrap code that could modify result
|
104
|
+
# to prevent the current result modification
|
105
|
+
def with_clean_result # :nodoc:
|
106
|
+
was_result = @result
|
107
|
+
yield
|
108
|
+
@result
|
109
|
+
ensure
|
110
|
+
@result = was_result
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns a result of applying the specified rule to the specified record.
|
114
|
+
# Under the hood a policy class for record is resolved
|
115
|
+
# (unless it's explicitly set through `with` option).
|
116
|
+
#
|
117
|
+
# If record is `nil` then we uses the current policy.
|
118
|
+
def allowed_to?(rule, record = :__undef__, **options)
|
119
|
+
if (record == :__undef__ || record == self.record) && options.empty?
|
120
|
+
__apply__(resolve_rule(rule))
|
121
|
+
else
|
122
|
+
policy_for(record: record, **options).then do |policy|
|
123
|
+
policy.apply(policy.resolve_rule(rule))
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# An alias for readability purposes
|
129
|
+
def check?(*args) ; allowed_to?(*args); end
|
130
|
+
|
131
|
+
# Returns a rule name (policy method name) for activity.
|
132
|
+
#
|
133
|
+
# By default, rule name is equal to activity name.
|
134
|
+
#
|
135
|
+
# Raises ActionPolicy::UknownRule when rule is not found in policy.
|
136
|
+
def resolve_rule(activity)
|
137
|
+
raise UnknownRule.new(self, activity) unless
|
138
|
+
respond_to?(activity)
|
139
|
+
activity
|
140
|
+
end
|
141
|
+
|
142
|
+
# Return annotated source code for the rule
|
143
|
+
# NOTE: require "method_source" and "unparser" gems to be installed.
|
144
|
+
# Otherwise returns empty string.
|
145
|
+
def inspect_rule(rule) ; PrettyPrint.print_method(self, rule); end
|
146
|
+
|
147
|
+
# Helper for printing the annotated rule source.
|
148
|
+
# Useful for debugging: type `pp :show?` within the context of the policy
|
149
|
+
# to preview the rule.
|
150
|
+
def pp(rule)
|
151
|
+
with_clean_result do
|
152
|
+
# We need result to exist for `allowed_to?` to work correctly
|
153
|
+
@result = self.class.result_class.new(self.class, rule)
|
154
|
+
header = "#{self.class.name}##{rule}"
|
155
|
+
source = inspect_rule(rule)
|
156
|
+
$stdout.puts "#{header}\n#{source}"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
module Policy
|
5
|
+
# Create default rules and aliases:
|
6
|
+
# - `index?` (=`false`)
|
7
|
+
# - `create?` (=`false`)
|
8
|
+
# - `new?` as an alias for `create?`
|
9
|
+
# - `manage?` as a fallback for all unspecified rules (default rule)
|
10
|
+
module Defaults
|
11
|
+
def self.included(base)
|
12
|
+
raise "Aliases support is required for defaults" unless
|
13
|
+
base.ancestors.include?(Aliases)
|
14
|
+
|
15
|
+
base.default_rule :manage?
|
16
|
+
base.alias_rule :new?, to: :create?
|
17
|
+
|
18
|
+
raise "Verification context support is required for defaults" unless
|
19
|
+
base.ancestors.include?(Aliases)
|
20
|
+
|
21
|
+
base.authorize :user
|
22
|
+
end
|
23
|
+
|
24
|
+
def index?() ; false; end
|
25
|
+
|
26
|
+
def create?() ; false; end
|
27
|
+
|
28
|
+
def manage?() ; false; end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
module Policy
|
5
|
+
# Result of applying a policy rule
|
6
|
+
#
|
7
|
+
# This class could be extended by some modules to provide
|
8
|
+
# additional functionality
|
9
|
+
class ExecutionResult
|
10
|
+
attr_reader :value, :policy, :rule
|
11
|
+
|
12
|
+
def initialize(policy, rule)
|
13
|
+
@policy = policy
|
14
|
+
@rule = rule
|
15
|
+
end
|
16
|
+
|
17
|
+
# Populate the final value
|
18
|
+
def load(value)
|
19
|
+
@value = value
|
20
|
+
end
|
21
|
+
|
22
|
+
def success?() ; @value == true; end
|
23
|
+
|
24
|
+
def fail?() ; @value == false; end
|
25
|
+
|
26
|
+
def cached!
|
27
|
+
@cached = true
|
28
|
+
end
|
29
|
+
|
30
|
+
def cached?() ; @cached == true; end
|
31
|
+
|
32
|
+
def inspect
|
33
|
+
"<#{policy}##{rule}: #{@value}>"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
module Policy
|
5
|
+
# Adds callback-style checks to policies to
|
6
|
+
# extract common checks from rules.
|
7
|
+
#
|
8
|
+
# class ApplicationPolicy < ActionPolicy::Base
|
9
|
+
# authorize :user
|
10
|
+
# pre_check :allow_admins
|
11
|
+
#
|
12
|
+
# private
|
13
|
+
# # Allow every action for admins
|
14
|
+
# def allow_admins
|
15
|
+
# allow! if user.admin?
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# You can specify conditional pre-checks (through `except` / `only`) options
|
20
|
+
# and skip already defined pre-checks if necessary.
|
21
|
+
#
|
22
|
+
# class UserPolicy < ApplicationPolicy
|
23
|
+
# skip_pre_check :allow_admins, only: :destroy?
|
24
|
+
#
|
25
|
+
# def destroy?
|
26
|
+
# user.admin? && !record.admin?
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
module PreCheck
|
30
|
+
# Single pre-check instance.
|
31
|
+
#
|
32
|
+
# Implements filtering logic.
|
33
|
+
class Check
|
34
|
+
attr_reader :name, :policy_class
|
35
|
+
|
36
|
+
def initialize(policy, name, except: nil, only: nil)
|
37
|
+
if !except.nil? && !only.nil?
|
38
|
+
raise ArgumentError,
|
39
|
+
"Only one of `except` and `only` may be specified for pre-check"
|
40
|
+
end
|
41
|
+
|
42
|
+
@policy_class = policy
|
43
|
+
@name = name
|
44
|
+
@blacklist = Array(except) unless except.nil?
|
45
|
+
@whitelist = Array(only) unless only.nil?
|
46
|
+
|
47
|
+
rebuild_filter
|
48
|
+
end
|
49
|
+
|
50
|
+
def applicable?(rule)
|
51
|
+
return true if filter.nil?
|
52
|
+
filter.call(rule)
|
53
|
+
end
|
54
|
+
|
55
|
+
def call(policy) ; policy.send(name); end
|
56
|
+
|
57
|
+
def skip!(except: nil, only: nil)
|
58
|
+
if !except.nil? && !only.nil?
|
59
|
+
raise ArgumentError,
|
60
|
+
"Only one of `except` and `only` may be specified when skipping pre-check"
|
61
|
+
end
|
62
|
+
|
63
|
+
if except.nil? && only.nil?
|
64
|
+
raise ArgumentError,
|
65
|
+
"At least one of `except` and `only` must be specified when skipping pre-check"
|
66
|
+
end
|
67
|
+
|
68
|
+
if except
|
69
|
+
@whitelist = Array(except)
|
70
|
+
@whitelist -= blacklist if blacklist
|
71
|
+
@blacklist = nil
|
72
|
+
else
|
73
|
+
# only
|
74
|
+
@blacklist += Array(only) if blacklist
|
75
|
+
@whitelist -= Array(only) if whitelist
|
76
|
+
@blacklist = Array(only) if filter.nil?
|
77
|
+
end
|
78
|
+
|
79
|
+
rebuild_filter
|
80
|
+
end
|
81
|
+
# rubocop: enable
|
82
|
+
# rubocop: enable
|
83
|
+
|
84
|
+
def dup
|
85
|
+
self.class.new(
|
86
|
+
policy_class,
|
87
|
+
name,
|
88
|
+
except: blacklist&.dup,
|
89
|
+
only: whitelist&.dup
|
90
|
+
)
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
attr_reader :whitelist, :blacklist, :filter
|
96
|
+
|
97
|
+
def rebuild_filter
|
98
|
+
@filter =
|
99
|
+
if whitelist
|
100
|
+
proc { |rule| whitelist.include?(rule) }
|
101
|
+
elsif blacklist
|
102
|
+
proc { |rule| !blacklist.include?(rule) }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class << self
|
108
|
+
def included(base)
|
109
|
+
base.extend ClassMethods
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def run_pre_checks(rule)
|
114
|
+
self.class.pre_checks.each do |check|
|
115
|
+
next unless check.applicable?(rule)
|
116
|
+
check.call(self)
|
117
|
+
end
|
118
|
+
|
119
|
+
yield if block_given?
|
120
|
+
end
|
121
|
+
|
122
|
+
def __apply__(rule)
|
123
|
+
run_pre_checks(rule) { super }
|
124
|
+
end
|
125
|
+
|
126
|
+
module ClassMethods # :nodoc:
|
127
|
+
def pre_check(*names, **options)
|
128
|
+
names.each do |name|
|
129
|
+
# do not allow pre-check override
|
130
|
+
check = pre_checks.find { _1.name == name }
|
131
|
+
raise "Pre-check already defined: #{name}" unless check.nil?
|
132
|
+
|
133
|
+
pre_checks << Check.new(self, name, **options)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def skip_pre_check(*names, **options)
|
138
|
+
names.each do |name|
|
139
|
+
check = pre_checks.find { _1.name == name }
|
140
|
+
raise "Pre-check not found: #{name}" if check.nil?
|
141
|
+
|
142
|
+
# when no options provided we remove this check completely
|
143
|
+
next pre_checks.delete(check) if options.empty?
|
144
|
+
|
145
|
+
# otherwise duplicate and apply skip options
|
146
|
+
pre_checks[pre_checks.index(check)] = check.dup.tap { _1.skip!(**options) }
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def pre_checks
|
151
|
+
return @pre_checks if instance_variable_defined?(:@pre_checks)
|
152
|
+
|
153
|
+
@pre_checks = if superclass.respond_to?(:pre_checks)
|
154
|
+
superclass.pre_checks.dup
|
155
|
+
else
|
156
|
+
[]
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
using RubyNext
|
5
|
+
|
6
|
+
module Policy
|
7
|
+
# Failures reasons store
|
8
|
+
class FailureReasons
|
9
|
+
attr_reader :reasons
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@reasons = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def add(policy_or_class, rule, details = nil)
|
16
|
+
policy_class = policy_or_class.is_a?(Module) ? policy_or_class : policy_or_class.class
|
17
|
+
reasons[policy_class] ||= []
|
18
|
+
|
19
|
+
if details.nil?
|
20
|
+
add_non_detailed_reason reasons[policy_class], rule
|
21
|
+
else
|
22
|
+
add_detailed_reason reasons[policy_class], with_details(rule, details)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Return Hash of the form:
|
27
|
+
# { policy_identifier => [rules, ...] }
|
28
|
+
def details() ; reasons.transform_keys(&:identifier); end
|
29
|
+
|
30
|
+
def empty?() ; reasons.empty?; end
|
31
|
+
|
32
|
+
def present?() ; !empty?; end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def add_non_detailed_reason(store, rule)
|
37
|
+
index =
|
38
|
+
if store.last.is_a?(::Hash)
|
39
|
+
store.size - 1
|
40
|
+
else
|
41
|
+
store.size
|
42
|
+
end
|
43
|
+
|
44
|
+
store.insert(index, rule)
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_detailed_reason(store, detailed_rule)
|
48
|
+
store.last.is_a?(::Hash) || store << {}
|
49
|
+
store.last.merge!(detailed_rule)
|
50
|
+
end
|
51
|
+
|
52
|
+
def with_details(rule, details)
|
53
|
+
return rule if details.nil?
|
54
|
+
|
55
|
+
{rule => details}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Extend ExecutionResult with `reasons` method
|
60
|
+
module ResultFailureReasons
|
61
|
+
def reasons
|
62
|
+
@reasons ||= FailureReasons.new
|
63
|
+
end
|
64
|
+
|
65
|
+
attr_accessor :details
|
66
|
+
|
67
|
+
def clear_details
|
68
|
+
@details = nil
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns all the details merged together
|
72
|
+
def all_details
|
73
|
+
return @all_details if defined?(@all_details)
|
74
|
+
|
75
|
+
@all_details = {}.tap do |all|
|
76
|
+
next unless defined?(@reasons)
|
77
|
+
|
78
|
+
reasons.reasons.each_value do |rules|
|
79
|
+
detailed_reasons = rules.last
|
80
|
+
|
81
|
+
next unless detailed_reasons.is_a?(Hash)
|
82
|
+
|
83
|
+
detailed_reasons.each_value do |details|
|
84
|
+
all.merge!(details)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Add reasons to inspect
|
91
|
+
def inspect
|
92
|
+
super.then do |str|
|
93
|
+
next str if reasons.empty?
|
94
|
+
str.sub(/>$/, " (reasons: #{reasons.details})")
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Provides failure reasons tracking functionality.
|
100
|
+
# That allows you to distinguish between the reasons why authorization was rejected.
|
101
|
+
#
|
102
|
+
# It's helpful when you compose policies (i.e. use one policy within another).
|
103
|
+
#
|
104
|
+
# For example:
|
105
|
+
#
|
106
|
+
# class ApplicantPolicy < ApplicationPolicy
|
107
|
+
# def show?
|
108
|
+
# user.has_permission?(:view_applicants) &&
|
109
|
+
# allowed_to?(:show?, object.stage)
|
110
|
+
# end
|
111
|
+
# end
|
112
|
+
#
|
113
|
+
# Now when you receive an exception, you have a reasons object, which contains additional
|
114
|
+
# information about the failure:
|
115
|
+
#
|
116
|
+
# rescue_from ActionPolicy::Unauthorized do |ex|
|
117
|
+
# ex.policy #=> ApplicantPolicy
|
118
|
+
# ex.rule #=> :show?
|
119
|
+
# ex.result.reasons.details #=> {stage: [:show?]}
|
120
|
+
# end
|
121
|
+
#
|
122
|
+
# NOTE: the reason key (`stage`) is a policy identifier (underscored class name by default).
|
123
|
+
# For namespaced policies it has a form of:
|
124
|
+
#
|
125
|
+
# class Admin::UserPolicy < ApplicationPolicy
|
126
|
+
# # ..
|
127
|
+
# end
|
128
|
+
#
|
129
|
+
# reasons.details #=> {:"admin/user" => [:show?]}
|
130
|
+
#
|
131
|
+
#
|
132
|
+
# You can also wrap _local_ rules into `allowed_to?` to populate reasons:
|
133
|
+
#
|
134
|
+
# class ApplicantPolicy < ApplicationPolicy
|
135
|
+
# def show?
|
136
|
+
# allowed_to?(:view_applicants?) &&
|
137
|
+
# allowed_to?(:show?, object.stage)
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# def view_applicants?
|
141
|
+
# user.has_permission?(:view_applicants)
|
142
|
+
# end
|
143
|
+
# end
|
144
|
+
#
|
145
|
+
# NOTE: there is `check?` alias for `allowed_to?`.
|
146
|
+
#
|
147
|
+
# You can provide additional details to your failure reasons by using
|
148
|
+
# a `details: { ... }` option:
|
149
|
+
#
|
150
|
+
# class ApplicantPolicy < ApplicationPolicy
|
151
|
+
# def show?
|
152
|
+
# allowed_to?(:show?, object.stage)
|
153
|
+
# end
|
154
|
+
# end
|
155
|
+
#
|
156
|
+
# class StagePolicy < ApplicationPolicy
|
157
|
+
# def show?
|
158
|
+
# # Add stage title to the failure reason (if any)
|
159
|
+
# # (could be used by client to show more descriptive message)
|
160
|
+
# details[:title] = record.title
|
161
|
+
#
|
162
|
+
# # then perform the checks
|
163
|
+
# user.stages.where(id: record.id).exists?
|
164
|
+
# end
|
165
|
+
# end
|
166
|
+
#
|
167
|
+
# # when accessing the reasons
|
168
|
+
# p ex.result.reasons.details #=> { stage: [{show?: {title: "Onboarding"}] }
|
169
|
+
#
|
170
|
+
# NOTE: when using detailed reasons, the `details` array contains as the last element
|
171
|
+
# a hash with ALL details reasons for the policy (in a form of <rule> => <details>).
|
172
|
+
#
|
173
|
+
module Reasons
|
174
|
+
class << self
|
175
|
+
def included(base)
|
176
|
+
base.result_class.prepend(ResultFailureReasons)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Add additional details to the failure reason
|
181
|
+
def details
|
182
|
+
result.details ||= {}
|
183
|
+
end
|
184
|
+
|
185
|
+
def allowed_to?(rule, record = :__undef__, **options)
|
186
|
+
res =
|
187
|
+
if (record == :__undef__ || record == self.record) && options.empty?
|
188
|
+
rule = resolve_rule(rule)
|
189
|
+
policy = self
|
190
|
+
with_clean_result { apply(rule) }
|
191
|
+
else
|
192
|
+
policy = policy_for(record: record, **options)
|
193
|
+
rule = policy.resolve_rule(rule)
|
194
|
+
|
195
|
+
policy.apply(rule)
|
196
|
+
policy.result
|
197
|
+
end
|
198
|
+
|
199
|
+
result&.reasons&.add(policy, rule, res.details) if res.fail?
|
200
|
+
|
201
|
+
res.clear_details
|
202
|
+
|
203
|
+
res.success?
|
204
|
+
end
|
205
|
+
|
206
|
+
def deny!(reason = nil)
|
207
|
+
result&.reasons&.add(self, reason) if reason
|
208
|
+
super()
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|