action_policy 0.5.0 → 0.5.1

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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/lib/.rbnext/2.7/action_policy/behaviours/policy_for.rb +62 -0
  3. data/lib/.rbnext/2.7/action_policy/i18n.rb +56 -0
  4. data/lib/.rbnext/2.7/action_policy/policy/cache.rb +101 -0
  5. data/lib/.rbnext/2.7/action_policy/policy/pre_check.rb +162 -0
  6. data/lib/.rbnext/2.7/action_policy/rspec/be_authorized_to.rb +89 -0
  7. data/lib/.rbnext/2.7/action_policy/rspec/have_authorized_scope.rb +124 -0
  8. data/lib/.rbnext/2.7/action_policy/utils/pretty_print.rb +159 -0
  9. data/lib/.rbnext/3.0/action_policy/behaviour.rb +115 -0
  10. data/lib/.rbnext/3.0/action_policy/behaviours/policy_for.rb +62 -0
  11. data/lib/.rbnext/3.0/action_policy/behaviours/scoping.rb +35 -0
  12. data/lib/.rbnext/3.0/action_policy/behaviours/thread_memoized.rb +59 -0
  13. data/lib/.rbnext/3.0/action_policy/ext/policy_cache_key.rb +72 -0
  14. data/lib/.rbnext/3.0/action_policy/policy/aliases.rb +69 -0
  15. data/lib/.rbnext/3.0/action_policy/policy/authorization.rb +87 -0
  16. data/lib/.rbnext/3.0/action_policy/policy/cache.rb +101 -0
  17. data/lib/.rbnext/3.0/action_policy/policy/core.rb +161 -0
  18. data/lib/.rbnext/3.0/action_policy/policy/defaults.rb +31 -0
  19. data/lib/.rbnext/3.0/action_policy/policy/execution_result.rb +37 -0
  20. data/lib/.rbnext/3.0/action_policy/policy/pre_check.rb +162 -0
  21. data/lib/.rbnext/3.0/action_policy/policy/reasons.rb +210 -0
  22. data/lib/.rbnext/3.0/action_policy/policy/scoping.rb +160 -0
  23. data/lib/.rbnext/3.0/action_policy/rspec/be_authorized_to.rb +89 -0
  24. data/lib/.rbnext/3.0/action_policy/rspec/have_authorized_scope.rb +124 -0
  25. data/lib/.rbnext/3.0/action_policy/utils/pretty_print.rb +159 -0
  26. data/lib/.rbnext/3.0/action_policy/utils/suggest_message.rb +19 -0
  27. data/lib/action_policy/version.rb +1 -1
  28. metadata +27 -2
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ module Behaviours
5
+ # Adds `authorized_scop` method to behaviour
6
+ module Scoping
7
+ # Apply scope to the target of the specified type.
8
+ #
9
+ # NOTE: policy lookup consists of the following steps:
10
+ # - first, check whether `with` option is present
11
+ # - secondly, try to infer policy class from `target` (non-raising lookup)
12
+ # - use `implicit_authorization_target` if none of the above works.
13
+ def authorized_scope(target, type: nil, as: :default, scope_options: nil, **options)
14
+ options[:context] && (options[:context] = authorization_context.merge(options[:context]))
15
+
16
+ policy = policy_for(record: target, allow_nil: true, **options)
17
+ policy ||= policy_for(record: implicit_authorization_target!, **options)
18
+
19
+ type ||= authorization_scope_type_for(policy, target)
20
+ name = as
21
+
22
+ Authorizer.scopify(target, policy, **{type: type, name: name, scope_options: scope_options})
23
+ end
24
+
25
+ # For backward compatibility
26
+ alias authorized authorized_scope
27
+
28
+ # Infer scope type for target if none provided.
29
+ # Raises an exception if type couldn't be inferred.
30
+ def authorization_scope_type_for(policy, target)
31
+ policy.resolve_scope_type(target)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ module PerThreadCache # :nodoc:
5
+ CACHE_KEY = "action_policy.per_thread_cache"
6
+
7
+ class << self
8
+ attr_writer :enabled
9
+
10
+ def enabled?() ; @enabled == true; end
11
+
12
+ def fetch(key)
13
+ return yield unless enabled?
14
+
15
+ store = (Thread.current[CACHE_KEY] ||= {})
16
+
17
+ return store[key] if store.key?(key)
18
+
19
+ store[key] = yield
20
+ end
21
+
22
+ def clear_all
23
+ Thread.current[CACHE_KEY] = {}
24
+ end
25
+ end
26
+
27
+ # Turn off by default in test env
28
+ self.enabled = !(ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test")
29
+ end
30
+
31
+ module Behaviours
32
+ # Per-thread memoization for policies.
33
+ #
34
+ # Used by `policy_for` to re-use policy object for records.
35
+ #
36
+ # NOTE: don't forget to clear thread cache with ActionPolicy::PerThreadCache.clear_all
37
+ module ThreadMemoized
38
+ class << self
39
+ def prepended(base)
40
+ base.prepend InstanceMethods
41
+ end
42
+
43
+ alias included prepended
44
+ end
45
+
46
+ module InstanceMethods # :nodoc:
47
+ def policy_for(record:, **opts)
48
+ __policy_thread_memoize__(record, **opts) { super(record: record, **opts) }
49
+ end
50
+ end
51
+
52
+ def __policy_thread_memoize__(record, **options)
53
+ cache_key = policy_for_cache_key(record: record, **options)
54
+
55
+ ActionPolicy::PerThreadCache.fetch(cache_key) { yield }
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ module Ext
5
+ # Adds #_policy_cache_key method to Object,
6
+ # which just call #policy_cache_key or #cache_key
7
+ # or #object_id (if `use_object_id` parameter is set to true).
8
+ #
9
+ # For other core classes returns string representation.
10
+ #
11
+ # Raises ArgumentError otherwise.
12
+ module PolicyCacheKey # :nodoc: all
13
+ module ObjectExt
14
+ def _policy_cache_key(use_object_id: false)
15
+ return policy_cache_key if respond_to?(:policy_cache_key)
16
+ return cache_key_with_version if respond_to?(:cache_key_with_version)
17
+ return cache_key if respond_to?(:cache_key)
18
+
19
+ return object_id.to_s if use_object_id == true
20
+
21
+ raise ArgumentError, "object is not cacheable"
22
+ end
23
+ end
24
+
25
+ refine Object do
26
+ include ObjectExt
27
+ end
28
+
29
+ refine NilClass do
30
+ def _policy_cache_key(*) ; ""; end
31
+ end
32
+
33
+ refine TrueClass do
34
+ def _policy_cache_key(*) ; "t"; end
35
+ end
36
+
37
+ refine FalseClass do
38
+ def _policy_cache_key(*) ; "f"; end
39
+ end
40
+
41
+ refine String do
42
+ def _policy_cache_key(*) ; self; end
43
+ end
44
+
45
+ refine Symbol do
46
+ def _policy_cache_key(*) ; to_s; end
47
+ end
48
+
49
+ if RUBY_PLATFORM.match?(/java/i)
50
+ refine Integer do
51
+ def _policy_cache_key(*) ; to_s; end
52
+ end
53
+
54
+ refine Float do
55
+ def _policy_cache_key(*) ; to_s; end
56
+ end
57
+ else
58
+ refine Numeric do
59
+ def _policy_cache_key(*) ; to_s; end
60
+ end
61
+ end
62
+
63
+ refine Time do
64
+ def _policy_cache_key(*) ; to_s; end
65
+ end
66
+
67
+ refine Module do
68
+ def _policy_cache_key(*) ; name; end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ module Policy
5
+ # Adds rules aliases support and ability to specify
6
+ # the default rule.
7
+ #
8
+ # class ApplicationPolicy
9
+ # include ActionPolicy::Policy::Core
10
+ # include ActionPolicy::Policy::Aliases
11
+ #
12
+ # # define which rule to use if `authorize!` called with
13
+ # # unknown rule
14
+ # default_rule :manage?
15
+ #
16
+ # alias_rule :publish?, :unpublish?, to: :update?
17
+ # end
18
+ #
19
+ # Aliases are used only during `authorize!` call (and do not act like _real_ aliases).
20
+ #
21
+ # Aliases useful when combined with `CachedApply` (since we can cache only the target rule).
22
+ module Aliases
23
+ DEFAULT = :__default__
24
+
25
+ class << self
26
+ def included(base)
27
+ base.extend ClassMethods
28
+ end
29
+ end
30
+
31
+ def resolve_rule(activity)
32
+ self.class.lookup_alias(activity) ||
33
+ (activity if respond_to?(activity)) ||
34
+ self.class.lookup_default_rule ||
35
+ super
36
+ end
37
+
38
+ module ClassMethods # :nodoc:
39
+ def default_rule(val)
40
+ rules_aliases[DEFAULT] = val
41
+ end
42
+
43
+ def alias_rule(*rules, to:)
44
+ rules.each do |rule|
45
+ rules_aliases[rule] = to
46
+ end
47
+ end
48
+
49
+ def lookup_alias(rule) ; rules_aliases[rule]; end
50
+
51
+ def lookup_default_rule() ; rules_aliases[DEFAULT]; end
52
+
53
+ def rules_aliases
54
+ return @rules_aliases if instance_variable_defined?(:@rules_aliases)
55
+
56
+ @rules_aliases = if superclass.respond_to?(:rules_aliases)
57
+ superclass.rules_aliases.dup
58
+ else
59
+ {}
60
+ end
61
+ end
62
+
63
+ def method_added(name)
64
+ rules_aliases.delete(name) if public_method_defined?(name)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ class AuthorizationContextMissing < Error # :nodoc:
5
+ MESSAGE_TEMPLATE = "Missing policy authorization context: %s"
6
+
7
+ attr_reader :message
8
+
9
+ def initialize(id)
10
+ @message = MESSAGE_TEMPLATE % id
11
+ end
12
+ end
13
+
14
+ module Policy
15
+ # Authorization context could include multiple parameters.
16
+ #
17
+ # It is possible to provide more verificatio contexts, by specifying them in the policy and
18
+ # providing them at the authorization step.
19
+ #
20
+ # For example:
21
+ #
22
+ # class ApplicationPolicy < ActionPolicy::Base
23
+ # # Add user and account to the context; it's required to be passed
24
+ # # to a policy constructor and be not nil
25
+ # authorize :user, :account
26
+ #
27
+ # # you can skip non-nil check if you want
28
+ # # authorize :account, allow_nil: true
29
+ #
30
+ # def manage?
31
+ # # available as a simple accessor
32
+ # account.enabled?
33
+ # end
34
+ # end
35
+ #
36
+ # ApplicantPolicy.new(user: user, account: account)
37
+ module Authorization
38
+ class << self
39
+ def included(base)
40
+ base.extend ClassMethods
41
+ end
42
+ end
43
+
44
+ attr_reader :authorization_context
45
+
46
+ def initialize(record = nil, **params)
47
+ super(record)
48
+
49
+ @authorization_context = {}
50
+
51
+ self.class.authorization_targets.each do |id, opts|
52
+ raise AuthorizationContextMissing, id unless params.key?(id) || opts[:optional]
53
+
54
+ val = params.fetch(id, nil)
55
+
56
+ raise AuthorizationContextMissing, id if val.nil? && opts[:allow_nil] != true
57
+
58
+ authorization_context[id] = instance_variable_set("@#{id}", val)
59
+ end
60
+
61
+ authorization_context.freeze
62
+ end
63
+
64
+ module ClassMethods # :nodoc:
65
+ def authorize(*ids, allow_nil: false, optional: false)
66
+ allow_nil ||= optional
67
+
68
+ ids.each do |id|
69
+ authorization_targets[id] = {allow_nil: allow_nil, optional: optional}
70
+ end
71
+
72
+ attr_reader(*ids)
73
+ end
74
+
75
+ def authorization_targets
76
+ return @authorization_targets if instance_variable_defined?(:@authorization_targets)
77
+
78
+ @authorization_targets = if superclass.respond_to?(:authorization_targets)
79
+ superclass.authorization_targets.dup
80
+ else
81
+ {}
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,101 @@
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._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 { _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.value
57
+ end
58
+ yield
59
+ store.write(key, result, options)
60
+ result.value
61
+ end
62
+ end
63
+
64
+ def apply(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
@@ -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