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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 939f6e73eca2c33b4f9d3575fde063ac346e46bcda7ab05c062ea9a35edf7252
|
4
|
+
data.tar.gz: 8c200d093dd9efa19d6cabd811c4ce29f7c4420efd3e8052c863adbf43c2a632
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 380f476a6716d6fc096d1b6abb6ef68eba62c4ad5fb761427754870582550a25394f24c0b29bab711785ab4806812b9c20ca0ec26f0f4efe7ddca11b762a9585
|
7
|
+
data.tar.gz: 0a63c058b49e2063af867ee74a78066e1e39f5a22d226748ee5780543e40287d672bfc177bedaf91f7036168ac72746a9128dc222a4d8a7bfe0690cdda7b5b49
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,16 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
## 0.5.5 (2020-12-28)
|
6
|
+
|
7
|
+
- Upgrade to Ruby 3.0. ([@palkan][])
|
8
|
+
|
9
|
+
## 0.5.4 (2020-12-09)
|
10
|
+
|
11
|
+
- Add support for RSpec aliases detection when linting policy specs with `rubocop-rspec` 2.0 ([@pirj][])
|
12
|
+
|
13
|
+
- Fix `strict_namespace: true` lookup option not finding policies in global namespace ([@Be-ngt-oH][])
|
14
|
+
|
5
15
|
## 0.5.0 (2020-09-29)
|
6
16
|
|
7
17
|
- Move `deny!` / `allow!` to core. ([@palkan][])
|
@@ -37,7 +47,7 @@ This method is similar to `allowed_to?` but returns an authorization result obje
|
|
37
47
|
|
38
48
|
Fixes [#122](https://github.com/palkan/action_policy/issues/122).
|
39
49
|
|
40
|
-
- Separated `#classify`-based and `#camelize`-based symbol lookups. ([Be-ngt-oH][])
|
50
|
+
- Separated `#classify`-based and `#camelize`-based symbol lookups. ([@Be-ngt-oH][])
|
41
51
|
|
42
52
|
Only affects Rails apps. Now lookup for `:users` tries to find `UsersPolicy` first (camelize),
|
43
53
|
and only then search for `UserPolicy` (classify).
|
@@ -425,7 +435,8 @@ This value is now stored in a cache (if any) instead of just the call result (`t
|
|
425
435
|
[@ilyasgaraev]: https://github.com/ilyasgaraev
|
426
436
|
[@brendon]: https://github.com/brendon
|
427
437
|
[@DmitryTsepelev]: https://github.com/DmitryTsepelev
|
428
|
-
[@korolvs]: https://github.com/
|
438
|
+
[@korolvs]: https://github.com/slavadev
|
429
439
|
[@nicolas-brousse]: https://github.com/nicolas-brousse
|
430
440
|
[@somenugget]: https://github.com/somenugget
|
431
441
|
[@Be-ngt-oH]: https://github.com/Be-ngt-oH
|
442
|
+
[@pirj]: https://github.com/pirj
|
@@ -0,0 +1,62 @@
|
|
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: authorization_context, allow_nil: false, default: default_authorization_policy_class)
|
12
|
+
policy_class = with || ::ActionPolicy.lookup(
|
13
|
+
record,
|
14
|
+
namespace: namespace, context: context, allow_nil: allow_nil, default: default
|
15
|
+
)
|
16
|
+
policy_class&.new(record, **context)
|
17
|
+
end
|
18
|
+
|
19
|
+
def authorization_context
|
20
|
+
raise NotImplementedError, "Please, define `authorization_context` method!"
|
21
|
+
end
|
22
|
+
|
23
|
+
def authorization_namespace
|
24
|
+
# override to provide specific authorization namespace
|
25
|
+
end
|
26
|
+
|
27
|
+
def default_authorization_policy_class
|
28
|
+
# override to provide a policy class use when no policy found
|
29
|
+
end
|
30
|
+
|
31
|
+
# Override this method to provide implicit authorization target
|
32
|
+
# that would be used in case `record` is not specified in
|
33
|
+
# `authorize!` and `allowed_to?` call.
|
34
|
+
#
|
35
|
+
# It is also used to infer a policy for scoping (in `authorized_scope` method).
|
36
|
+
def implicit_authorization_target
|
37
|
+
# no-op
|
38
|
+
end
|
39
|
+
|
40
|
+
# Return implicit authorization target or raises an exception if it's nil
|
41
|
+
def implicit_authorization_target!
|
42
|
+
implicit_authorization_target || raise(
|
43
|
+
NotFound,
|
44
|
+
[
|
45
|
+
self,
|
46
|
+
"Couldn't find implicit authorization target " \
|
47
|
+
"for #{self.class}. " \
|
48
|
+
"Please, provide policy class explicitly using `with` option or " \
|
49
|
+
"define the `implicit_authorization_target` method."
|
50
|
+
]
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **)
|
55
|
+
record_key = record._policy_cache_key(use_object_id: true)
|
56
|
+
context_key = context.values.map { _1._policy_cache_key(use_object_id: true) }.join(".")
|
57
|
+
|
58
|
+
"#{namespace}/#{with}/#{context_key}/#{record_key}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -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_method :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,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,159 @@
|
|
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 "parser/current"
|
11
|
+
require "unparser"
|
12
|
+
rescue LoadError
|
13
|
+
# do nothing
|
14
|
+
ensure
|
15
|
+
$VERBOSE = old_verbose
|
16
|
+
end
|
17
|
+
|
18
|
+
module ActionPolicy
|
19
|
+
using RubyNext
|
20
|
+
|
21
|
+
# Takes the object and a method name,
|
22
|
+
# and returns the "annotated" source code for the method:
|
23
|
+
# code is split into parts by logical operators and each
|
24
|
+
# part is evaluated separately.
|
25
|
+
#
|
26
|
+
# Example:
|
27
|
+
#
|
28
|
+
# class MyClass
|
29
|
+
# def access?
|
30
|
+
# admin? && access_feed?
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# puts PrettyPrint.format_method(MyClass.new, :access?)
|
35
|
+
#
|
36
|
+
# #=> MyClass#access?
|
37
|
+
# #=> ↳ admin? #=> false
|
38
|
+
# #=> AND
|
39
|
+
# #=> access_feed? #=> true
|
40
|
+
module PrettyPrint
|
41
|
+
TRUE = "\e[32mtrue\e[0m"
|
42
|
+
FALSE = "\e[31mfalse\e[0m"
|
43
|
+
|
44
|
+
class Visitor
|
45
|
+
attr_reader :lines, :object
|
46
|
+
attr_accessor :indent
|
47
|
+
|
48
|
+
def initialize(object)
|
49
|
+
@object = object
|
50
|
+
end
|
51
|
+
|
52
|
+
def collect(ast)
|
53
|
+
@lines = []
|
54
|
+
@indent = 0
|
55
|
+
|
56
|
+
visit_node(ast)
|
57
|
+
|
58
|
+
lines.join("\n")
|
59
|
+
end
|
60
|
+
|
61
|
+
def visit_node(ast)
|
62
|
+
if respond_to?("visit_#{ast.type}")
|
63
|
+
send("visit_#{ast.type}", ast)
|
64
|
+
else
|
65
|
+
visit_missing ast
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def expression_with_result(sexp)
|
70
|
+
expression = Unparser.unparse(sexp)
|
71
|
+
"#{expression} #=> #{PrettyPrint.colorize(eval_exp(expression))}"
|
72
|
+
end
|
73
|
+
|
74
|
+
def eval_exp(exp)
|
75
|
+
return "<skipped>" if ignore_exp?(exp)
|
76
|
+
object.instance_eval(exp)
|
77
|
+
rescue => e
|
78
|
+
"Failed: #{e.message}"
|
79
|
+
end
|
80
|
+
|
81
|
+
def visit_and(ast)
|
82
|
+
visit_node(ast.children[0])
|
83
|
+
lines << indented("AND")
|
84
|
+
visit_node(ast.children[1])
|
85
|
+
end
|
86
|
+
|
87
|
+
def visit_or(ast)
|
88
|
+
visit_node(ast.children[0])
|
89
|
+
lines << indented("OR")
|
90
|
+
visit_node(ast.children[1])
|
91
|
+
end
|
92
|
+
|
93
|
+
def visit_begin(ast)
|
94
|
+
# Parens
|
95
|
+
if ast.children.size == 1
|
96
|
+
lines << indented("(")
|
97
|
+
self.indent += 2
|
98
|
+
visit_node(ast.children[0])
|
99
|
+
self.indent -= 2
|
100
|
+
lines << indented(")")
|
101
|
+
else
|
102
|
+
# Multiple expressions
|
103
|
+
ast.children.each do |node|
|
104
|
+
visit_node(node)
|
105
|
+
# restore indent after each expression
|
106
|
+
self.indent -= 2
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def visit_missing(ast)
|
112
|
+
lines << indented(expression_with_result(ast))
|
113
|
+
end
|
114
|
+
|
115
|
+
def indented(str)
|
116
|
+
"#{indent.zero? ? "↳ " : ""}#{" " * indent}#{str}".tap do
|
117
|
+
# increase indent after the first expression
|
118
|
+
self.indent += 2 if indent.zero?
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Some lines should not be evaled
|
123
|
+
def ignore_exp?(exp)
|
124
|
+
PrettyPrint.ignore_expressions.any? { exp.match?(_1) }
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class << self
|
129
|
+
attr_accessor :ignore_expressions
|
130
|
+
|
131
|
+
if defined?(::Unparser) && defined?(::MethodSource)
|
132
|
+
def available?() = true
|
133
|
+
|
134
|
+
def print_method(object, method_name)
|
135
|
+
ast = object.method(method_name).source.then(&Unparser.method(:parse))
|
136
|
+
# outer node is a method definition itself
|
137
|
+
body = ast.children[2]
|
138
|
+
|
139
|
+
Visitor.new(object).collect(body)
|
140
|
+
end
|
141
|
+
else
|
142
|
+
def available?() = false
|
143
|
+
|
144
|
+
def print_method(_, _) = ""
|
145
|
+
end
|
146
|
+
|
147
|
+
def colorize(val)
|
148
|
+
return val unless $stdout.isatty
|
149
|
+
return TRUE if val.eql?(true)
|
150
|
+
return FALSE if val.eql?(false)
|
151
|
+
val
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
self.ignore_expressions = [
|
156
|
+
/^\s*binding\.(pry|irb)\s*$/s
|
157
|
+
]
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,62 @@
|
|
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: authorization_context, allow_nil: false, default: default_authorization_policy_class)
|
12
|
+
policy_class = with || ::ActionPolicy.lookup(
|
13
|
+
record,
|
14
|
+
namespace: namespace, context: context, allow_nil: allow_nil, default: default
|
15
|
+
)
|
16
|
+
policy_class&.new(record, **context)
|
17
|
+
end
|
18
|
+
|
19
|
+
def authorization_context
|
20
|
+
raise NotImplementedError, "Please, define `authorization_context` method!"
|
21
|
+
end
|
22
|
+
|
23
|
+
def authorization_namespace
|
24
|
+
# override to provide specific authorization namespace
|
25
|
+
end
|
26
|
+
|
27
|
+
def default_authorization_policy_class
|
28
|
+
# override to provide a policy class use when no policy found
|
29
|
+
end
|
30
|
+
|
31
|
+
# Override this method to provide implicit authorization target
|
32
|
+
# that would be used in case `record` is not specified in
|
33
|
+
# `authorize!` and `allowed_to?` call.
|
34
|
+
#
|
35
|
+
# It is also used to infer a policy for scoping (in `authorized_scope` method).
|
36
|
+
def implicit_authorization_target
|
37
|
+
# no-op
|
38
|
+
end
|
39
|
+
|
40
|
+
# Return implicit authorization target or raises an exception if it's nil
|
41
|
+
def implicit_authorization_target!
|
42
|
+
implicit_authorization_target || raise(
|
43
|
+
NotFound,
|
44
|
+
[
|
45
|
+
self,
|
46
|
+
"Couldn't find implicit authorization target " \
|
47
|
+
"for #{self.class}. " \
|
48
|
+
"Please, provide policy class explicitly using `with` option or " \
|
49
|
+
"define the `implicit_authorization_target` method."
|
50
|
+
]
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **)
|
55
|
+
record_key = record._policy_cache_key(use_object_id: true)
|
56
|
+
context_key = context.values.map { |_1| _1._policy_cache_key(use_object_id: true) }.join(".")
|
57
|
+
|
58
|
+
"#{namespace}/#{with}/#{context_key}/#{record_key}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|