action_policy 0.5.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
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