action_policy 0.7.3 → 0.7.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 21363e4337fbe0caea24309750d79d94bb9fa6c4ae0a9d0697b0e686519fa763
4
- data.tar.gz: 8ac572534b621240640723ea5c6ec942a73b91e14f2d40e534889c2dc7ba9251
3
+ metadata.gz: c2ca6d3ab4293001cc4b07e7fe5503a8c06a8b72684c0f7a9c4e7a4ef8a32c89
4
+ data.tar.gz: f1e726704611a3bc9ade5d3466fd64c4a5c9faddf5aa8338f3179ec4da2381e5
5
5
  SHA512:
6
- metadata.gz: c1dd33d739cfdf1143b7cc2402c2e8bf7ebf1f1613fa7c650c72fddf0bd4fd4b5c924c920ff143ad582ab68524836f0d0c32641372c5999fdd95f6d9113078dc
7
- data.tar.gz: 5fb9dbd9b1f19fb6bed19ddbeb99be9bd35495b2d0f89bd3fb0cd8fed59a0513c4a627fdd25291b56dcfc7cf9b60f64d351bb2b658d97351f9a7b5db2d1c4128
6
+ metadata.gz: aa35a3efefa37fc64066a78899b9b03a2283e2458471425a2e7cc2bc0b4ff829e87beda744057866f661e37fc54f14d094ba0fc446ea155537042478109576a7
7
+ data.tar.gz: d90069b7a1b3bea22c2d5e6dd990231f1bf24f85fd50081a92a54bcd5c271faa42f40c9c526a8434abd793e6f4c928b5cb28d4dad3fbaeb2bf3701f67fd4fd21
data/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.7.4 (2025-03-12)
6
+
7
+ - Let authorize! return the policy record ([@sedubois][])
8
+
9
+ - Enable `allowance_to` as a helper method by default ([@stephannv][])
10
+
11
+ - Allow the `:through` option of `authorize` to be passed a proc ([@brendon][])
12
+
5
13
  ## 0.7.3 (2024-12-18)
6
14
 
7
15
  - Fix keeping the result object in concurrent (Fiber-ed) execution environments. ([@palkan][])
@@ -539,3 +547,5 @@ This value is now stored in a cache (if any) instead of just the call result (`t
539
547
  [@matsales28]: https://github.com/matsales28
540
548
  [@killondark]: https://github.com/killondark
541
549
  [@Spone]: https://github.com/Spone
550
+ [@stephannv]: https://github.com/stephannv
551
+ [@sedubois]: https://github.com/sedubois
@@ -78,8 +78,6 @@ module ActionPolicy
78
78
 
79
79
  rebuild_filter
80
80
  end
81
- # rubocop: enable
82
- # rubocop: enable
83
81
 
84
82
  def dup
85
83
  self.class.new(
@@ -27,6 +27,8 @@ module ActionPolicy
27
27
  # { policy_identifier => [rules, ...] }
28
28
  def details() ; reasons.transform_keys(&:identifier); end
29
29
 
30
+ alias_method :to_h, :details
31
+
30
32
  def empty?() ; reasons.empty?; end
31
33
 
32
34
  def present?() ; !empty?; end
@@ -33,11 +33,13 @@ module ActionPolicy
33
33
  # Policy is inferred from record
34
34
  # (unless explicitly specified through `with` option).
35
35
  #
36
+ # @return the policy record
36
37
  # Raises `ActionPolicy::Unauthorized` if check failed.
37
38
  def authorize!(record = :__undef__, to:, **options)
38
39
  policy = lookup_authorization_policy(record, **options)
39
40
 
40
41
  Authorizer.call(policy, authorization_rule_for(policy, to))
42
+ policy.record
41
43
  end
42
44
 
43
45
  # Checks that an activity is allowed for the current context (e.g. user).
@@ -62,8 +64,8 @@ module ActionPolicy
62
64
 
63
65
  private def build_authorization_context
64
66
  self.class.authorization_targets
65
- .each_with_object({}) do |(key, meth), obj|
66
- obj[key] = send(meth)
67
+ .each_with_object({}) do |(key, method_or_proc), obj|
68
+ obj[key] = method_or_proc.is_a?(Proc) ? method_or_proc.call : send(method_or_proc)
67
69
  end
68
70
  end
69
71
 
@@ -103,8 +105,8 @@ module ActionPolicy
103
105
  # authorize :user
104
106
  # end
105
107
  def authorize(key, through: nil)
106
- meth = through || key
107
- authorization_targets[key] = meth
108
+ method_or_proc = through || key
109
+ authorization_targets[key] = method_or_proc
108
110
  end
109
111
 
110
112
  def authorization_targets
@@ -78,8 +78,6 @@ module ActionPolicy
78
78
 
79
79
  rebuild_filter
80
80
  end
81
- # rubocop: enable
82
- # rubocop: enable
83
81
 
84
82
  def dup
85
83
  self.class.new(
@@ -27,6 +27,8 @@ module ActionPolicy
27
27
  # { policy_identifier => [rules, ...] }
28
28
  def details() = reasons.transform_keys(&:identifier)
29
29
 
30
+ alias_method :to_h, :details
31
+
30
32
  def empty?() = reasons.empty?
31
33
 
32
34
  def present?() = !empty?
@@ -26,6 +26,7 @@ module ActionPolicy
26
26
  if respond_to?(:helper_method)
27
27
  helper_method :allowed_to?
28
28
  helper_method :authorized_scope
29
+ helper_method :allowance_to
29
30
  end
30
31
 
31
32
  attr_writer :authorize_count
@@ -44,13 +45,15 @@ module ActionPolicy
44
45
  # If record is not provided, tries to infer the resource class
45
46
  # from controller name (i.e. `controller_name.classify.safe_constantize`).
46
47
  #
48
+ # @return the policy record
47
49
  # Raises `ActionPolicy::Unauthorized` if check failed.
48
50
  def authorize!(record = :__undef__, to: nil, **options)
49
51
  to ||= :"#{action_name}?"
50
52
 
51
- super
53
+ policy_record = super
52
54
 
53
55
  self.authorize_count += 1
56
+ policy_record
54
57
  end
55
58
 
56
59
  # Tries to infer the resource class from controller name
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionPolicy
4
- VERSION = "0.7.3"
4
+ VERSION = "0.7.4"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: action_policy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.3
4
+ version: 0.7.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-18 00:00:00.000000000 Z
11
+ date: 2025-03-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-next-core
@@ -133,15 +133,6 @@ files:
133
133
  - LICENSE.txt
134
134
  - README.md
135
135
  - config/rubocop-rspec.yml
136
- - lib/.rbnext/2.7/action_policy/behaviours/policy_for.rb
137
- - lib/.rbnext/2.7/action_policy/i18n.rb
138
- - lib/.rbnext/2.7/action_policy/policy/cache.rb
139
- - lib/.rbnext/2.7/action_policy/policy/pre_check.rb
140
- - lib/.rbnext/2.7/action_policy/rails/scope_matchers/action_controller_params.rb
141
- - lib/.rbnext/2.7/action_policy/rails/scope_matchers/active_record.rb
142
- - lib/.rbnext/2.7/action_policy/rspec/be_authorized_to.rb
143
- - lib/.rbnext/2.7/action_policy/rspec/have_authorized_scope.rb
144
- - lib/.rbnext/2.7/action_policy/utils/pretty_print.rb
145
136
  - lib/.rbnext/3.0/action_policy/behaviours/policy_for.rb
146
137
  - lib/.rbnext/3.0/action_policy/behaviours/thread_memoized.rb
147
138
  - lib/.rbnext/3.0/action_policy/ext/policy_cache_key.rb
@@ -152,6 +143,8 @@ files:
152
143
  - lib/.rbnext/3.0/action_policy/policy/execution_result.rb
153
144
  - lib/.rbnext/3.0/action_policy/policy/pre_check.rb
154
145
  - lib/.rbnext/3.0/action_policy/policy/reasons.rb
146
+ - lib/.rbnext/3.0/action_policy/rails/scope_matchers/action_controller_params.rb
147
+ - lib/.rbnext/3.0/action_policy/rails/scope_matchers/active_record.rb
155
148
  - lib/.rbnext/3.0/action_policy/rspec/be_an_alias_of.rb
156
149
  - lib/.rbnext/3.0/action_policy/rspec/be_authorized_to.rb
157
150
  - lib/.rbnext/3.0/action_policy/rspec/have_authorized_scope.rb
@@ -234,7 +227,7 @@ metadata:
234
227
  documentation_uri: https://actionpolicy.evilmartians.io/
235
228
  homepage_uri: https://actionpolicy.evilmartians.io/
236
229
  source_code_uri: http://github.com/palkan/action_policy
237
- post_install_message:
230
+ post_install_message:
238
231
  rdoc_options: []
239
232
  require_paths:
240
233
  - lib
@@ -250,7 +243,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
250
243
  version: '0'
251
244
  requirements: []
252
245
  rubygems_version: 3.4.19
253
- signing_key:
246
+ signing_key:
254
247
  specification_version: 4
255
248
  summary: Authorization framework for Ruby/Rails application
256
249
  test_files: []
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionPolicy
4
- module Behaviours
5
- # Adds `policy_for` method
6
- module PolicyFor
7
- require "action_policy/ext/policy_cache_key"
8
- using ActionPolicy::Ext::PolicyCacheKey
9
-
10
- # Returns policy instance for the record.
11
- def policy_for(record:, with: nil, namespace: authorization_namespace, context: nil, allow_nil: false, default: default_authorization_policy_class, strict_namespace: authorization_strict_namespace)
12
- context = context ? build_authorization_context.merge(context) : authorization_context
13
-
14
- policy_class = with || ::ActionPolicy.lookup(
15
- record,
16
- namespace: namespace, context: context, allow_nil: allow_nil, default: default, strict_namespace: strict_namespace
17
- )
18
- policy_class&.new(record, **context)
19
- end
20
-
21
- def authorization_context ; @authorization_context ||= build_authorization_context; end
22
-
23
- def build_authorization_context
24
- Kernel.raise NotImplementedError, "Please, define `build_authorization_context` method!"
25
- end
26
-
27
- def authorization_namespace
28
- # override to provide specific authorization namespace
29
- end
30
-
31
- def default_authorization_policy_class
32
- # override to provide a policy class use when no policy found
33
- end
34
-
35
- def authorization_strict_namespace
36
- # override to provide strict namespace lookup option
37
- end
38
-
39
- # Override this method to provide implicit authorization target
40
- # that would be used in case `record` is not specified in
41
- # `authorize!` and `allowed_to?` call.
42
- #
43
- # It is also used to infer a policy for scoping (in `authorized_scope` method).
44
- def implicit_authorization_target
45
- # no-op
46
- end
47
-
48
- # Return implicit authorization target or raises an exception if it's nil
49
- def implicit_authorization_target!
50
- implicit_authorization_target || Kernel.raise(
51
- NotFound,
52
- [
53
- self,
54
- "Couldn't find implicit authorization target " \
55
- "for #{self.class}. " \
56
- "Please, provide policy class explicitly using `with` option or " \
57
- "define the `implicit_authorization_target` method."
58
- ]
59
- )
60
- end
61
-
62
- def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **__kwrest__)
63
- record_key = record._policy_cache_key(use_object_id: true)
64
- context_key = context.values.map { |_1| _1._policy_cache_key(use_object_id: true) }.join(".")
65
-
66
- "#{namespace}/#{with}/#{context_key}/#{record_key}"
67
- end
68
- end
69
- end
70
- end
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionPolicy
4
- module I18n # :nodoc:
5
- DEFAULT_UNAUTHORIZED_MESSAGE = "You are not authorized to perform this action"
6
-
7
- class << self
8
- def full_message(policy_class, rule, details = nil)
9
- candidates = candidates_for(policy_class, rule)
10
-
11
- options = {scope: :action_policy}
12
- options.merge!(details) unless details.nil?
13
-
14
- ::I18n.t(
15
- candidates.shift,
16
- default: candidates,
17
- **options
18
- )
19
- end
20
-
21
- private
22
-
23
- def candidates_for(policy_class, rule)
24
- policy_hierarchy = policy_class.ancestors.select { |_1| _1.respond_to?(:identifier) }
25
- [
26
- *policy_hierarchy.map { |klass| :"policy.#{klass.identifier}.#{rule}" },
27
- :"policy.#{rule}",
28
- :unauthorized,
29
- DEFAULT_UNAUTHORIZED_MESSAGE
30
- ]
31
- end
32
- end
33
-
34
- ActionPolicy::Policy::FailureReasons.prepend(Module.new do
35
- def full_messages
36
- reasons.flat_map do |policy_klass, rules|
37
- rules.flat_map do |rule|
38
- if rule.is_a?(::Hash)
39
- rule.map do |key, details|
40
- ActionPolicy::I18n.full_message(policy_klass, key, details)
41
- end
42
- else
43
- ActionPolicy::I18n.full_message(policy_klass, rule)
44
- end
45
- end
46
- end
47
- end
48
- end)
49
-
50
- ActionPolicy::Policy::ExecutionResult.prepend(Module.new do
51
- def message
52
- ActionPolicy::I18n.full_message(policy, rule, details)
53
- end
54
- end)
55
- end
56
- end
@@ -1,101 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "action_policy/version"
4
-
5
- module ActionPolicy # :nodoc:
6
- using RubyNext
7
-
8
- # By default cache namespace (or prefix) contains major and minor version of the gem
9
- CACHE_NAMESPACE = "acp:#{ActionPolicy::VERSION.split(".").take(2).join(".")}"
10
-
11
- require "action_policy/ext/policy_cache_key"
12
-
13
- using ActionPolicy::Ext::PolicyCacheKey
14
-
15
- module Policy
16
- # Provides long-lived cache through ActionPolicy.cache_store.
17
- #
18
- # NOTE: if cache_store is nil then we silently skip all the caching.
19
- module Cache
20
- class << self
21
- def included(base)
22
- base.extend ClassMethods
23
- end
24
- end
25
-
26
- def cache_namespace() ; ActionPolicy::CACHE_NAMESPACE; end
27
-
28
- def cache_key(*parts)
29
- [
30
- cache_namespace,
31
- *parts
32
- ].map { |_1| _1._policy_cache_key }.join("/")
33
- end
34
-
35
- def rule_cache_key(rule)
36
- cache_key(
37
- context_cache_key,
38
- record,
39
- self.class,
40
- rule
41
- )
42
- end
43
-
44
- def context_cache_key
45
- authorization_context.map { |_1, _2| _2._policy_cache_key.to_s }.join("/")
46
- end
47
-
48
- def apply_with_cache(rule)
49
- options = self.class.cached_rules.fetch(rule)
50
- key = rule_cache_key(rule)
51
-
52
- ActionPolicy.cache_store.then do |store|
53
- result = store.read(key)
54
- unless result.nil?
55
- result.cached!
56
- next result
57
- end
58
- yield.tap do |result|
59
- store.write(key, result, options)
60
- end
61
- end
62
- end
63
-
64
- def apply_r(rule)
65
- return super if ActionPolicy.cache_store.nil? ||
66
- !self.class.cached_rules.key?(rule)
67
-
68
- apply_with_cache(rule) { super }
69
- end
70
-
71
- def cache(*parts, **options)
72
- key = cache_key(*parts)
73
- ActionPolicy.cache_store.then do |store|
74
- res = store.read(key)
75
- next res unless res.nil?
76
- res = yield
77
- store.write(key, res, options)
78
- res
79
- end
80
- end
81
-
82
- module ClassMethods # :nodoc:
83
- def cache(*rules, **options)
84
- rules.each do |rule|
85
- cached_rules[rule] = options
86
- end
87
- end
88
-
89
- def cached_rules
90
- return @cached_rules if instance_variable_defined?(:@cached_rules)
91
-
92
- @cached_rules = if superclass.respond_to?(:cached_rules)
93
- superclass.cached_rules.dup
94
- else
95
- {}
96
- end
97
- end
98
- end
99
- end
100
- end
101
- end
@@ -1,162 +0,0 @@
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| _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| _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| _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
@@ -1,96 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "action_policy/testing"
4
-
5
- module ActionPolicy
6
- module RSpec
7
- # Authorization matcher `be_authorized_to`.
8
- #
9
- # Verifies that a block of code has been authorized using specific policy.
10
- #
11
- # Example:
12
- #
13
- # # in controller/request specs
14
- # subject { patch :update, id: product.id }
15
- #
16
- # it "is authorized" do
17
- # expect { subject }
18
- # .to be_authorized_to(:manage?, product)
19
- # .with(ProductPolicy)
20
- # end
21
- #
22
- class BeAuthorizedTo < ::RSpec::Matchers::BuiltIn::BaseMatcher
23
- attr_reader :rule, :target, :policy, :actual_calls, :context
24
-
25
- def initialize(rule, target)
26
- @rule = rule
27
- @target = target
28
- end
29
-
30
- def with(policy)
31
- @policy = policy
32
- self
33
- end
34
-
35
- def with_context(context)
36
- @context = context
37
- self
38
- end
39
-
40
- def match(_expected, actual)
41
- raise "This matcher only supports block expectations" unless actual.is_a?(Proc)
42
-
43
- @policy ||= ::ActionPolicy.lookup(target)
44
- @context ||= nil
45
-
46
- begin
47
- ActionPolicy::Testing::AuthorizeTracker.tracking { actual.call }
48
- rescue ActionPolicy::Unauthorized
49
- # we don't want to care about authorization result
50
- end
51
-
52
- @actual_calls = ActionPolicy::Testing::AuthorizeTracker.calls
53
-
54
- actual_calls.any? { |_1| _1.matches?(policy, rule, target, context) }
55
- end
56
-
57
- def does_not_match?(*__rest__)
58
- raise "This matcher doesn't support negation"
59
- end
60
-
61
- def supports_block_expectations?() ; true; end
62
-
63
- def failure_message
64
- "expected #{formatted_record} " \
65
- "to be authorized with #{policy}##{rule}, " \
66
- "#{context ? "and context #{context.inspect}, " : ""}" \
67
- "but #{actual_calls_message}"
68
- end
69
-
70
- def actual_calls_message
71
- if actual_calls.empty?
72
- "no authorization calls have been made"
73
- else
74
- "the following calls were encountered:\n" \
75
- "#{formatted_calls}"
76
- end
77
- end
78
-
79
- def formatted_calls
80
- actual_calls.map do |_1|
81
- " - #{_1.inspect}"
82
- end.join("\n")
83
- end
84
-
85
- def formatted_record(record = target) ; ::RSpec::Support::ObjectFormatter.format(record); end
86
- end
87
- end
88
- end
89
-
90
- RSpec.configure do |config|
91
- config.include(Module.new do
92
- def be_authorized_to(rule, target)
93
- ActionPolicy::RSpec::BeAuthorizedTo.new(rule, target)
94
- end
95
- end)
96
- end
@@ -1,130 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "action_policy/testing"
4
-
5
- module ActionPolicy
6
- module RSpec
7
- # Implements `have_authorized_scope` matcher.
8
- #
9
- # Verifies that a block of code applies authorization scoping using specific policy.
10
- #
11
- # Example:
12
- #
13
- # # in controller/request specs
14
- # subject { get :index }
15
- #
16
- # it "has authorized scope" do
17
- # expect { subject }
18
- # .to have_authorized_scope(:active_record_relation)
19
- # .with(ProductPolicy)
20
- # end
21
- #
22
- class HaveAuthorizedScope < ::RSpec::Matchers::BuiltIn::BaseMatcher
23
- attr_reader :type, :name, :policy, :scope_options, :actual_scopes,
24
- :target_expectations, :context
25
-
26
- def initialize(type)
27
- @type = type
28
- @name = :default
29
- @scope_options = nil
30
- end
31
-
32
- def with(policy)
33
- @policy = policy
34
- self
35
- end
36
-
37
- def as(name)
38
- @name = name
39
- self
40
- end
41
-
42
- def with_scope_options(scope_options)
43
- @scope_options = scope_options
44
- self
45
- end
46
-
47
- def with_target(&block)
48
- @target_expectations = block
49
- self
50
- end
51
-
52
- def with_context(context)
53
- @context = context
54
- self
55
- end
56
-
57
- def match(_expected, actual)
58
- raise "This matcher only supports block expectations" unless actual.is_a?(Proc)
59
-
60
- ActionPolicy::Testing::AuthorizeTracker.tracking { actual.call }
61
-
62
- @actual_scopes = ActionPolicy::Testing::AuthorizeTracker.scopings
63
-
64
- matching_scopes = actual_scopes.select { |_1| _1.matches?(policy, type, name, scope_options, context) }
65
-
66
- return false if matching_scopes.empty?
67
-
68
- return true unless target_expectations
69
-
70
- if matching_scopes.size > 1
71
- raise "Too many matching scopings (#{matching_scopes.size}), " \
72
- "you can run `.with_target` only when there is the only one match"
73
- end
74
-
75
- target_expectations.call(matching_scopes.first.target)
76
- true
77
- end
78
-
79
- def does_not_match?(*__rest__)
80
- raise "This matcher doesn't support negation"
81
- end
82
-
83
- def supports_block_expectations?() ; true; end
84
-
85
- def failure_message
86
- "expected a scoping named :#{name} for type :#{type} " \
87
- "#{scope_options_message} " \
88
- "#{context ? "and context #{context.inspect} " : ""}" \
89
- "from #{policy} to have been applied, " \
90
- "but #{actual_scopes_message}"
91
- end
92
-
93
- def scope_options_message
94
- if scope_options
95
- if defined?(::RSpec::Matchers::Composable) &&
96
- scope_options.is_a?(::RSpec::Matchers::Composable)
97
- "with scope options #{scope_options.description}"
98
- else
99
- "with scope options #{scope_options}"
100
- end
101
- else
102
- "without scope options"
103
- end
104
- end
105
-
106
- def actual_scopes_message
107
- if actual_scopes.empty?
108
- "no scopings have been made"
109
- else
110
- "the following scopings were encountered:\n" \
111
- "#{formatted_scopings}"
112
- end
113
- end
114
-
115
- def formatted_scopings
116
- actual_scopes.map do |_1|
117
- " - #{_1.inspect}"
118
- end.join("\n")
119
- end
120
- end
121
- end
122
- end
123
-
124
- RSpec.configure do |config|
125
- config.include(Module.new do
126
- def have_authorized_scope(type)
127
- ActionPolicy::RSpec::HaveAuthorizedScope.new(type)
128
- end
129
- end)
130
- end
@@ -1,155 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- old_verbose = $VERBOSE
4
-
5
- begin
6
- require "method_source"
7
- # Ignore parse warnings when patch
8
- # Ruby version mismatches
9
- $VERBOSE = nil
10
- require "prism"
11
- rescue LoadError
12
- # do nothing
13
- ensure
14
- $VERBOSE = old_verbose
15
- end
16
-
17
- module ActionPolicy
18
- using RubyNext
19
-
20
- # Takes the object and a method name,
21
- # and returns the "annotated" source code for the method:
22
- # code is split into parts by logical operators and each
23
- # part is evaluated separately.
24
- #
25
- # Example:
26
- #
27
- # class MyClass
28
- # def access?
29
- # admin? && access_feed?
30
- # end
31
- # end
32
- #
33
- # puts PrettyPrint.format_method(MyClass.new, :access?)
34
- #
35
- # #=> MyClass#access?
36
- # #=> ↳ admin? #=> false
37
- # #=> AND
38
- # #=> access_feed? #=> true
39
- module PrettyPrint
40
- TRUE = "\e[32mtrue\e[0m"
41
- FALSE = "\e[31mfalse\e[0m"
42
-
43
- class Visitor
44
- attr_reader :lines, :object, :source
45
- attr_accessor :indent
46
-
47
- def initialize(object)
48
- @object = object
49
- end
50
-
51
- def collect(ast)
52
- @lines = []
53
- @indent = 0
54
- @source = ast.source.source
55
- ast = ast.value.child_nodes[0].child_nodes[0].body
56
-
57
- visit_node(ast)
58
-
59
- lines.join("\n")
60
- end
61
-
62
- def visit_node(ast)
63
- if respond_to?("visit_#{ast.type}")
64
- send("visit_#{ast.type}", ast)
65
- else
66
- visit_missing ast
67
- end
68
- end
69
-
70
- def expression_with_result(sexp)
71
- expression = source[sexp.location.start_offset...sexp.location.end_offset]
72
- "#{expression} #=> #{PrettyPrint.colorize(eval_exp(expression))}"
73
- end
74
-
75
- def eval_exp(exp)
76
- return "<skipped>" if ignore_exp?(exp)
77
- object.instance_eval(exp)
78
- rescue => e
79
- "Failed: #{e.message}"
80
- end
81
-
82
- def visit_and_node(ast)
83
- visit_node(ast.left)
84
- lines << indented("AND")
85
- visit_node(ast.right)
86
- end
87
-
88
- def visit_or_node(ast)
89
- visit_node(ast.left)
90
- lines << indented("OR")
91
- visit_node(ast.right)
92
- end
93
-
94
- def visit_statements_node(ast)
95
- ast.child_nodes.each do |node|
96
- visit_node(node)
97
- # restore indent after each expression
98
- self.indent -= 2
99
- end
100
- end
101
-
102
- def visit_parentheses_node(ast)
103
- lines << indented("(")
104
- self.indent += 2
105
- visit_node(ast.child_nodes[0])
106
- lines << indented(")")
107
- end
108
-
109
- def visit_missing(ast)
110
- lines << indented(expression_with_result(ast))
111
- end
112
-
113
- def indented(str)
114
- "#{indent.zero? ? "↳ " : ""}#{" " * indent}#{str}".tap do
115
- # increase indent after the first expression
116
- self.indent += 2 if indent.zero?
117
- end
118
- end
119
-
120
- # Some lines should not be evaled
121
- def ignore_exp?(exp)
122
- PrettyPrint.ignore_expressions.any? { |_1| exp.match?(_1) }
123
- end
124
- end
125
-
126
- class << self
127
- attr_accessor :ignore_expressions
128
-
129
- if defined?(::Prism) && defined?(::MethodSource)
130
- def available?() ; true; end
131
-
132
- def print_method(object, method_name)
133
- ast = Prism.parse(object.method(method_name).source)
134
-
135
- Visitor.new(object).collect(ast)
136
- end
137
- else
138
- def available?() ; false; end
139
-
140
- def print_method(_, _) ; ""; end
141
- end
142
-
143
- def colorize(val)
144
- return val unless $stdout.isatty
145
- return TRUE if val.eql?(true) # rubocop:disable Lint/DeprecatedConstants
146
- return FALSE if val.eql?(false) # rubocop:disable Lint/DeprecatedConstants
147
- val
148
- end
149
- end
150
-
151
- self.ignore_expressions = [
152
- /^\s*binding\.(pry|irb)\s*$/s
153
- ]
154
- end
155
- end