action_policy 0.6.7 → 0.6.8
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 +4 -0
- data/LICENSE.txt +1 -1
- data/lib/.rbnext/2.7/action_policy/behaviours/policy_for.rb +1 -1
- data/lib/.rbnext/2.7/action_policy/rails/scope_matchers/action_controller_params.rb +5 -3
- data/lib/.rbnext/2.7/action_policy/rails/scope_matchers/active_record.rb +13 -11
- data/lib/.rbnext/2.7/action_policy/rspec/be_authorized_to.rb +1 -1
- data/lib/.rbnext/2.7/action_policy/rspec/have_authorized_scope.rb +1 -1
- data/lib/.rbnext/3.0/action_policy/ext/policy_cache_key.rb +10 -10
- data/lib/.rbnext/3.0/action_policy/policy/core.rb +1 -1
- data/lib/.rbnext/3.0/action_policy/rspec/be_authorized_to.rb +1 -1
- data/lib/.rbnext/3.0/action_policy/rspec/have_authorized_scope.rb +1 -1
- data/lib/.rbnext/3.0/action_policy/utils/suggest_message.rb +1 -1
- data/lib/.rbnext/3.1/action_policy/behaviours/policy_for.rb +1 -1
- data/lib/.rbnext/3.1/action_policy/ext/policy_cache_key.rb +10 -10
- data/lib/.rbnext/3.2/action_policy/behaviours/policy_for.rb +68 -0
- data/lib/.rbnext/3.2/action_policy/ext/policy_cache_key.rb +72 -0
- data/lib/.rbnext/3.2/action_policy/lookup_chain.rb +145 -0
- data/lib/.rbnext/3.2/action_policy/policy/core.rb +168 -0
- data/lib/.rbnext/3.2/action_policy/rspec/be_authorized_to.rb +96 -0
- data/lib/.rbnext/3.2/action_policy/rspec/have_authorized_scope.rb +124 -0
- data/lib/.rbnext/3.2/action_policy/utils/suggest_message.rb +19 -0
- data/lib/action_policy/rails/scope_matchers/action_controller_params.rb +5 -3
- data/lib/action_policy/rails/scope_matchers/active_record.rb +13 -11
- data/lib/action_policy/railtie.rb +4 -15
- data/lib/action_policy/version.rb +1 -1
- metadata +12 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1e78f24a8faa881e8cad62f6bb4fc58921668fb7d41ac933c7fb1934117acc7d
|
4
|
+
data.tar.gz: 39f55de2e98b9a1d67519df40452349a2007e8d7168039438d2c8469cc7779e7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 66b0de36683bc63ce349b0fba097585b7fcb4d7d65145da53c26cd5d6f0c798ef76c447d75a987c1f16110d3d4bb379153407ef79b531613cc6cfa8e55171570
|
7
|
+
data.tar.gz: b7eadbbd0020e60addce888bf8287722c8bafd32a8b35ff2c66d8ea51eaa3b9c6f406e5214fdcddda4e7d4d527d12f08be3f86eac64d28db3b62b7976de28f17
|
data/CHANGELOG.md
CHANGED
data/LICENSE.txt
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
The MIT License (MIT)
|
2
2
|
|
3
|
-
Copyright (c) 2018-
|
3
|
+
Copyright (c) 2018-2024 Vladimir Dementyev
|
4
4
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
@@ -57,7 +57,7 @@ module ActionPolicy
|
|
57
57
|
)
|
58
58
|
end
|
59
59
|
|
60
|
-
def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **)
|
60
|
+
def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **__kwrest__)
|
61
61
|
record_key = record._policy_cache_key(use_object_id: true)
|
62
62
|
context_key = context.values.map { |_1| _1._policy_cache_key(use_object_id: true) }.join(".")
|
63
63
|
|
@@ -12,8 +12,10 @@ module ActionPolicy
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
-
# Register params scope matcher
|
16
|
-
ActionPolicy::Base.scope_matcher :action_controller_params, ActionController::Parameters
|
17
|
-
|
18
15
|
# Add alias to base policy
|
19
16
|
ActionPolicy::Base.extend ActionPolicy::ScopeMatchers::ActionControllerParams
|
17
|
+
|
18
|
+
ActiveSupport.on_load(:action_controller) do
|
19
|
+
# Register params scope matcher
|
20
|
+
ActionPolicy::Base.scope_matcher :action_controller_params, ActionController::Parameters
|
21
|
+
end
|
@@ -12,18 +12,20 @@ module ActionPolicy
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
-
# Register relation scope matcher
|
16
|
-
ActionPolicy::Base.scope_matcher :active_record_relation, ActiveRecord::Relation
|
17
|
-
|
18
15
|
# Add alias to base policy
|
19
16
|
ActionPolicy::Base.extend ActionPolicy::ScopeMatchers::ActiveRecord
|
20
17
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
18
|
+
ActiveSupport.on_load(:active_record) do
|
19
|
+
# Register relation scope matcher
|
20
|
+
ActionPolicy::Base.scope_matcher :active_record_relation, ActiveRecord::Relation
|
21
|
+
|
22
|
+
ActiveRecord::Relation.include(Module.new do
|
23
|
+
def policy_name
|
24
|
+
if model.respond_to?(:policy_name)
|
25
|
+
model.policy_name.to_s
|
26
|
+
else
|
27
|
+
"#{model}Policy"
|
28
|
+
end
|
27
29
|
end
|
28
|
-
end
|
29
|
-
end
|
30
|
+
end)
|
31
|
+
end
|
@@ -27,45 +27,45 @@ module ActionPolicy
|
|
27
27
|
end
|
28
28
|
|
29
29
|
refine NilClass do
|
30
|
-
def _policy_cache_key(*) ; ""; end
|
30
|
+
def _policy_cache_key(*__rest__) ; ""; end
|
31
31
|
end
|
32
32
|
|
33
33
|
refine TrueClass do
|
34
|
-
def _policy_cache_key(*) ; "t"; end
|
34
|
+
def _policy_cache_key(*__rest__) ; "t"; end
|
35
35
|
end
|
36
36
|
|
37
37
|
refine FalseClass do
|
38
|
-
def _policy_cache_key(*) ; "f"; end
|
38
|
+
def _policy_cache_key(*__rest__) ; "f"; end
|
39
39
|
end
|
40
40
|
|
41
41
|
refine String do
|
42
|
-
def _policy_cache_key(*) ; self; end
|
42
|
+
def _policy_cache_key(*__rest__) ; self; end
|
43
43
|
end
|
44
44
|
|
45
45
|
refine Symbol do
|
46
|
-
def _policy_cache_key(*) ; to_s; end
|
46
|
+
def _policy_cache_key(*__rest__) ; to_s; end
|
47
47
|
end
|
48
48
|
|
49
49
|
if RUBY_PLATFORM.match?(/java/i)
|
50
50
|
refine Integer do
|
51
|
-
def _policy_cache_key(*) ; to_s; end
|
51
|
+
def _policy_cache_key(*__rest__) ; to_s; end
|
52
52
|
end
|
53
53
|
|
54
54
|
refine Float do
|
55
|
-
def _policy_cache_key(*) ; to_s; end
|
55
|
+
def _policy_cache_key(*__rest__) ; to_s; end
|
56
56
|
end
|
57
57
|
else
|
58
58
|
refine Numeric do
|
59
|
-
def _policy_cache_key(*) ; to_s; end
|
59
|
+
def _policy_cache_key(*__rest__) ; to_s; end
|
60
60
|
end
|
61
61
|
end
|
62
62
|
|
63
63
|
refine Time do
|
64
|
-
def _policy_cache_key(*) ; to_s; end
|
64
|
+
def _policy_cache_key(*__rest__) ; to_s; end
|
65
65
|
end
|
66
66
|
|
67
67
|
refine Module do
|
68
|
-
def _policy_cache_key(*) ; name; end
|
68
|
+
def _policy_cache_key(*__rest__) ; name; end
|
69
69
|
end
|
70
70
|
end
|
71
71
|
end
|
@@ -57,7 +57,7 @@ module ActionPolicy
|
|
57
57
|
)
|
58
58
|
end
|
59
59
|
|
60
|
-
def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **)
|
60
|
+
def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **__kwrest__)
|
61
61
|
record_key = record._policy_cache_key(use_object_id: true)
|
62
62
|
context_key = context.values.map { _1._policy_cache_key(use_object_id: true) }.join(".")
|
63
63
|
|
@@ -27,45 +27,45 @@ module ActionPolicy
|
|
27
27
|
end
|
28
28
|
|
29
29
|
refine NilClass do
|
30
|
-
def _policy_cache_key(*) = ""
|
30
|
+
def _policy_cache_key(*__rest__) = ""
|
31
31
|
end
|
32
32
|
|
33
33
|
refine TrueClass do
|
34
|
-
def _policy_cache_key(*) = "t"
|
34
|
+
def _policy_cache_key(*__rest__) = "t"
|
35
35
|
end
|
36
36
|
|
37
37
|
refine FalseClass do
|
38
|
-
def _policy_cache_key(*) = "f"
|
38
|
+
def _policy_cache_key(*__rest__) = "f"
|
39
39
|
end
|
40
40
|
|
41
41
|
refine String do
|
42
|
-
def _policy_cache_key(*) = self
|
42
|
+
def _policy_cache_key(*__rest__) = self
|
43
43
|
end
|
44
44
|
|
45
45
|
refine Symbol do
|
46
|
-
def _policy_cache_key(*) = to_s
|
46
|
+
def _policy_cache_key(*__rest__) = to_s
|
47
47
|
end
|
48
48
|
|
49
49
|
if RUBY_PLATFORM.match?(/java/i)
|
50
50
|
refine Integer do
|
51
|
-
def _policy_cache_key(*) = to_s
|
51
|
+
def _policy_cache_key(*__rest__) = to_s
|
52
52
|
end
|
53
53
|
|
54
54
|
refine Float do
|
55
|
-
def _policy_cache_key(*) = to_s
|
55
|
+
def _policy_cache_key(*__rest__) = to_s
|
56
56
|
end
|
57
57
|
else
|
58
58
|
refine Numeric do
|
59
|
-
def _policy_cache_key(*) = to_s
|
59
|
+
def _policy_cache_key(*__rest__) = to_s
|
60
60
|
end
|
61
61
|
end
|
62
62
|
|
63
63
|
refine Time do
|
64
|
-
def _policy_cache_key(*) = to_s
|
64
|
+
def _policy_cache_key(*__rest__) = to_s
|
65
65
|
end
|
66
66
|
|
67
67
|
refine Module do
|
68
|
-
def _policy_cache_key(*) = name
|
68
|
+
def _policy_cache_key(*__rest__) = name
|
69
69
|
end
|
70
70
|
end
|
71
71
|
end
|
@@ -0,0 +1,68 @@
|
|
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 ? authorization_context.merge(context) : authorization_context
|
13
|
+
|
14
|
+
policy_class = with || ::ActionPolicy.lookup(
|
15
|
+
record,
|
16
|
+
namespace:, context:, allow_nil:, default:, strict_namespace:
|
17
|
+
)
|
18
|
+
policy_class&.new(record, **context)
|
19
|
+
end
|
20
|
+
|
21
|
+
def authorization_context
|
22
|
+
Kernel.raise NotImplementedError, "Please, define `authorization_context` method!"
|
23
|
+
end
|
24
|
+
|
25
|
+
def authorization_namespace
|
26
|
+
# override to provide specific authorization namespace
|
27
|
+
end
|
28
|
+
|
29
|
+
def default_authorization_policy_class
|
30
|
+
# override to provide a policy class use when no policy found
|
31
|
+
end
|
32
|
+
|
33
|
+
def authorization_strict_namespace
|
34
|
+
# override to provide strict namespace lookup option
|
35
|
+
end
|
36
|
+
|
37
|
+
# Override this method to provide implicit authorization target
|
38
|
+
# that would be used in case `record` is not specified in
|
39
|
+
# `authorize!` and `allowed_to?` call.
|
40
|
+
#
|
41
|
+
# It is also used to infer a policy for scoping (in `authorized_scope` method).
|
42
|
+
def implicit_authorization_target
|
43
|
+
# no-op
|
44
|
+
end
|
45
|
+
|
46
|
+
# Return implicit authorization target or raises an exception if it's nil
|
47
|
+
def implicit_authorization_target!
|
48
|
+
implicit_authorization_target || Kernel.raise(
|
49
|
+
NotFound,
|
50
|
+
[
|
51
|
+
self,
|
52
|
+
"Couldn't find implicit authorization target " \
|
53
|
+
"for #{self.class}. " \
|
54
|
+
"Please, provide policy class explicitly using `with` option or " \
|
55
|
+
"define the `implicit_authorization_target` method."
|
56
|
+
]
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **__kwrest__)
|
61
|
+
record_key = record._policy_cache_key(use_object_id: true)
|
62
|
+
context_key = context.values.map { _1._policy_cache_key(use_object_id: true) }.join(".")
|
63
|
+
|
64
|
+
"#{namespace}/#{with}/#{context_key}/#{record_key}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
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
|
+
import_methods ObjectExt
|
27
|
+
end
|
28
|
+
|
29
|
+
refine NilClass do
|
30
|
+
def _policy_cache_key(*__rest__) = ""
|
31
|
+
end
|
32
|
+
|
33
|
+
refine TrueClass do
|
34
|
+
def _policy_cache_key(*__rest__) = "t"
|
35
|
+
end
|
36
|
+
|
37
|
+
refine FalseClass do
|
38
|
+
def _policy_cache_key(*__rest__) = "f"
|
39
|
+
end
|
40
|
+
|
41
|
+
refine String do
|
42
|
+
def _policy_cache_key(*__rest__) = self
|
43
|
+
end
|
44
|
+
|
45
|
+
refine Symbol do
|
46
|
+
def _policy_cache_key(*__rest__) = to_s
|
47
|
+
end
|
48
|
+
|
49
|
+
if RUBY_PLATFORM.match?(/java/i)
|
50
|
+
refine Integer do
|
51
|
+
def _policy_cache_key(*__rest__) = to_s
|
52
|
+
end
|
53
|
+
|
54
|
+
refine Float do
|
55
|
+
def _policy_cache_key(*__rest__) = to_s
|
56
|
+
end
|
57
|
+
else
|
58
|
+
refine Numeric do
|
59
|
+
def _policy_cache_key(*__rest__) = to_s
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
refine Time do
|
64
|
+
def _policy_cache_key(*__rest__) = to_s
|
65
|
+
end
|
66
|
+
|
67
|
+
refine Module do
|
68
|
+
def _policy_cache_key(*__rest__) = name
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
# LookupChain contains _resolvers_ to determine a policy
|
5
|
+
# for a record (with additional options).
|
6
|
+
#
|
7
|
+
# You can modify the `LookupChain.chain` (for example, to add
|
8
|
+
# custom resolvers).
|
9
|
+
module LookupChain
|
10
|
+
unless "".respond_to?(:safe_constantize)
|
11
|
+
require "action_policy/ext/string_constantize"
|
12
|
+
using ActionPolicy::Ext::StringConstantize
|
13
|
+
end
|
14
|
+
|
15
|
+
require "action_policy/ext/symbol_camelize"
|
16
|
+
using ActionPolicy::Ext::SymbolCamelize
|
17
|
+
|
18
|
+
require "action_policy/ext/module_namespace"
|
19
|
+
using ActionPolicy::Ext::ModuleNamespace
|
20
|
+
|
21
|
+
# Cache namespace resolving result for policies.
|
22
|
+
# @see benchmarks/namespaced_lookup_cache.rb
|
23
|
+
class NamespaceCache
|
24
|
+
class << self
|
25
|
+
attr_reader :store
|
26
|
+
|
27
|
+
def put_if_absent(scope, namespace, policy)
|
28
|
+
local_store = store[scope][namespace]
|
29
|
+
return local_store[policy] if local_store[policy]
|
30
|
+
local_store[policy] ||= yield
|
31
|
+
end
|
32
|
+
|
33
|
+
def fetch(namespace, policy, strict:, &block)
|
34
|
+
return yield unless LookupChain.namespace_cache_enabled?
|
35
|
+
|
36
|
+
if strict
|
37
|
+
put_if_absent(:strict, namespace, policy, &block)
|
38
|
+
else
|
39
|
+
put_if_absent(:flexible, namespace, policy, &block)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def clear
|
44
|
+
hash = Hash.new { |h, k| h[k] = {} }
|
45
|
+
@store = {strict: hash, flexible: hash.clone}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
clear
|
50
|
+
end
|
51
|
+
|
52
|
+
class << self
|
53
|
+
attr_accessor :chain, :namespace_cache_enabled
|
54
|
+
|
55
|
+
alias_method :namespace_cache_enabled?, :namespace_cache_enabled
|
56
|
+
|
57
|
+
def call(record, **opts)
|
58
|
+
chain.each do |probe|
|
59
|
+
val = probe.call(record, **opts)
|
60
|
+
return val unless val.nil?
|
61
|
+
end
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def lookup_within_namespace(policy_name, namespace, strict: false)
|
68
|
+
NamespaceCache.fetch(namespace&.name || "Kernel", policy_name, strict: strict) do
|
69
|
+
mod = namespace
|
70
|
+
policy_class = nil
|
71
|
+
|
72
|
+
loop do
|
73
|
+
policy_class = [mod&.name, policy_name].compact.join("::").safe_constantize
|
74
|
+
break policy_class if policy_class || mod.nil?
|
75
|
+
|
76
|
+
mod = mod.namespace
|
77
|
+
end
|
78
|
+
|
79
|
+
next policy_class if !strict || namespace.nil? || policy_class.nil?
|
80
|
+
|
81
|
+
# If we're in the strict mode and the namespace boundary is provided,
|
82
|
+
# we must check that the found policy satisfies it
|
83
|
+
policy_class if policy_class.name.start_with?("#{namespace.name}::")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def policy_class_name_for(record)
|
88
|
+
return record.policy_name.to_s if record.respond_to?(:policy_name)
|
89
|
+
|
90
|
+
record_class = record.is_a?(Module) ? record : record.class
|
91
|
+
|
92
|
+
if record_class.respond_to?(:policy_name)
|
93
|
+
record_class.policy_name.to_s
|
94
|
+
else
|
95
|
+
"#{record_class}Policy"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Enable namespace cache by default or
|
101
|
+
# if RACK_ENV provided and equal to "production"
|
102
|
+
self.namespace_cache_enabled =
|
103
|
+
(!ENV["RACK_ENV"].nil?) ? ENV["RACK_ENV"] == "production" : true
|
104
|
+
|
105
|
+
# By self `policy_class` method
|
106
|
+
INSTANCE_POLICY_CLASS = ->(record, **__kwrest__) {
|
107
|
+
record.policy_class if record.respond_to?(:policy_class)
|
108
|
+
}
|
109
|
+
|
110
|
+
# By record's class `policy_class` method
|
111
|
+
CLASS_POLICY_CLASS = ->(record, **__kwrest__) {
|
112
|
+
record.class.policy_class if record.class.respond_to?(:policy_class)
|
113
|
+
}
|
114
|
+
|
115
|
+
# Infer from class name
|
116
|
+
INFER_FROM_CLASS = ->(record, namespace: nil, strict_namespace: false, **__kwrest__) {
|
117
|
+
policy_name = policy_class_name_for(record)
|
118
|
+
lookup_within_namespace(policy_name, namespace, strict: strict_namespace)
|
119
|
+
}
|
120
|
+
|
121
|
+
# Infer from passed symbol
|
122
|
+
SYMBOL_LOOKUP = ->(record, namespace: nil, strict_namespace: false, **__kwrest__) {
|
123
|
+
next unless record.is_a?(Symbol)
|
124
|
+
|
125
|
+
policy_name = "#{record.camelize}Policy"
|
126
|
+
lookup_within_namespace(policy_name, namespace, strict: strict_namespace)
|
127
|
+
}
|
128
|
+
|
129
|
+
# (Optional) Infer using String#classify if available
|
130
|
+
CLASSIFY_SYMBOL_LOOKUP = ->(record, namespace: nil, strict_namespace: false, **__kwrest__) {
|
131
|
+
next unless record.is_a?(Symbol)
|
132
|
+
|
133
|
+
policy_name = "#{record.to_s.classify}Policy"
|
134
|
+
lookup_within_namespace(policy_name, namespace, strict: strict_namespace)
|
135
|
+
}
|
136
|
+
|
137
|
+
self.chain = [
|
138
|
+
SYMBOL_LOOKUP,
|
139
|
+
(CLASSIFY_SYMBOL_LOOKUP if String.method_defined?(:classify)),
|
140
|
+
INSTANCE_POLICY_CLASS,
|
141
|
+
CLASS_POLICY_CLASS,
|
142
|
+
INFER_FROM_CLASS
|
143
|
+
].compact
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,168 @@
|
|
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 = "Couldn't find rule '#{@rule}' for #{@policy}" \
|
27
|
+
"#{suggest(@rule, @policy.instance_methods - Object.instance_methods)}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class NonPredicateRule < UnknownRule
|
32
|
+
def initialize(policy, rule)
|
33
|
+
@policy = policy.class
|
34
|
+
@rule = rule
|
35
|
+
@message = "The rule '#{@rule}' of '#{@policy}' must ends with ? (question mark)\nDid you mean? #{@rule}?"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module Policy
|
40
|
+
# Core policy API
|
41
|
+
module Core
|
42
|
+
class << self
|
43
|
+
def included(base)
|
44
|
+
base.extend ClassMethods
|
45
|
+
|
46
|
+
# Generate a new class for each _policy chain_
|
47
|
+
# in order to extend it independently
|
48
|
+
base.module_eval do
|
49
|
+
@result_class = Class.new(ExecutionResult)
|
50
|
+
|
51
|
+
# we need to make this class _named_,
|
52
|
+
# 'cause anonymous classes couldn't be marshalled
|
53
|
+
base.const_set(:APR, @result_class)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
module ClassMethods # :nodoc:
|
59
|
+
attr_writer :identifier
|
60
|
+
|
61
|
+
def result_class
|
62
|
+
return @result_class if instance_variable_defined?(:@result_class)
|
63
|
+
@result_class = superclass.result_class
|
64
|
+
end
|
65
|
+
|
66
|
+
def identifier
|
67
|
+
return @identifier if instance_variable_defined?(:@identifier)
|
68
|
+
|
69
|
+
@identifier = name.sub(/Policy$/, "").underscore.to_sym
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
include ActionPolicy::Behaviours::PolicyFor
|
74
|
+
|
75
|
+
attr_reader :record, :result
|
76
|
+
|
77
|
+
# NEXT_RELEASE: deprecate `record` arg, migrate to `record: nil`
|
78
|
+
def initialize(record = nil, *__rest__)
|
79
|
+
@record = record
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns a result of applying the specified rule (true of false).
|
83
|
+
# Unlike simply calling a predicate rule (`policy.manage?`),
|
84
|
+
# `apply` also calls pre-checks.
|
85
|
+
def apply(rule)
|
86
|
+
@result = self.class.result_class.new(self.class, rule)
|
87
|
+
|
88
|
+
catch :policy_fulfilled do
|
89
|
+
result.load __apply__(resolve_rule(rule))
|
90
|
+
end
|
91
|
+
|
92
|
+
result.value
|
93
|
+
end
|
94
|
+
|
95
|
+
def deny!
|
96
|
+
result&.load false
|
97
|
+
throw :policy_fulfilled
|
98
|
+
end
|
99
|
+
|
100
|
+
def allow!
|
101
|
+
result&.load true
|
102
|
+
throw :policy_fulfilled
|
103
|
+
end
|
104
|
+
|
105
|
+
# This method performs the rule call.
|
106
|
+
# Override or extend it to provide custom functionality
|
107
|
+
# (such as caching, pre checks, etc.)
|
108
|
+
def __apply__(rule) = public_send(rule)
|
109
|
+
|
110
|
+
# Wrap code that could modify result
|
111
|
+
# to prevent the current result modification
|
112
|
+
def with_clean_result # :nodoc:
|
113
|
+
was_result = @result
|
114
|
+
yield
|
115
|
+
@result
|
116
|
+
ensure
|
117
|
+
@result = was_result
|
118
|
+
end
|
119
|
+
|
120
|
+
# Returns a result of applying the specified rule to the specified record.
|
121
|
+
# Under the hood a policy class for record is resolved
|
122
|
+
# (unless it's explicitly set through `with` option).
|
123
|
+
#
|
124
|
+
# If record is `nil` then we uses the current policy.
|
125
|
+
def allowed_to?(rule, record = :__undef__, **options)
|
126
|
+
if (record == :__undef__ || record == self.record) && options.empty?
|
127
|
+
__apply__(resolve_rule(rule))
|
128
|
+
else
|
129
|
+
policy_for(record: record, **options).then do |policy|
|
130
|
+
policy.apply(policy.resolve_rule(rule))
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# An alias for readability purposes
|
136
|
+
def check?(*args, **hargs) = allowed_to?(*args, **hargs)
|
137
|
+
|
138
|
+
# Returns a rule name (policy method name) for activity.
|
139
|
+
#
|
140
|
+
# By default, rule name is equal to activity name.
|
141
|
+
#
|
142
|
+
# Raises ActionPolicy::UnknownRule when rule is not found in policy.
|
143
|
+
def resolve_rule(activity)
|
144
|
+
raise UnknownRule.new(self, activity) unless
|
145
|
+
respond_to?(activity)
|
146
|
+
activity
|
147
|
+
end
|
148
|
+
|
149
|
+
# Return annotated source code for the rule
|
150
|
+
# NOTE: require "method_source" and "unparser" gems to be installed.
|
151
|
+
# Otherwise returns empty string.
|
152
|
+
def inspect_rule(rule) = PrettyPrint.print_method(self, rule)
|
153
|
+
|
154
|
+
# Helper for printing the annotated rule source.
|
155
|
+
# Useful for debugging: type `pp :show?` within the context of the policy
|
156
|
+
# to preview the rule.
|
157
|
+
def pp(rule)
|
158
|
+
with_clean_result do
|
159
|
+
# We need result to exist for `allowed_to?` to work correctly
|
160
|
+
@result = self.class.result_class.new(self.class, rule)
|
161
|
+
header = "#{self.class.name}##{rule}"
|
162
|
+
source = inspect_rule(rule)
|
163
|
+
$stdout.puts "#{header}\n#{source}"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,96 @@
|
|
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.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
|
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
|
81
|
+
" - #{_1.inspect}"
|
82
|
+
end.join("\n")
|
83
|
+
end
|
84
|
+
|
85
|
+
def formatted_record(record = target) = ::RSpec::Support::ObjectFormatter.format(record)
|
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
|
@@ -0,0 +1,124 @@
|
|
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
|
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 match(_expected, actual)
|
53
|
+
raise "This matcher only supports block expectations" unless actual.is_a?(Proc)
|
54
|
+
|
55
|
+
ActionPolicy::Testing::AuthorizeTracker.tracking { actual.call }
|
56
|
+
|
57
|
+
@actual_scopes = ActionPolicy::Testing::AuthorizeTracker.scopings
|
58
|
+
|
59
|
+
matching_scopes = actual_scopes.select { _1.matches?(policy, type, name, scope_options) }
|
60
|
+
|
61
|
+
return false if matching_scopes.empty?
|
62
|
+
|
63
|
+
return true unless target_expectations
|
64
|
+
|
65
|
+
if matching_scopes.size > 1
|
66
|
+
raise "Too many matching scopings (#{matching_scopes.size}), " \
|
67
|
+
"you can run `.with_target` only when there is the only one match"
|
68
|
+
end
|
69
|
+
|
70
|
+
target_expectations.call(matching_scopes.first.target)
|
71
|
+
true
|
72
|
+
end
|
73
|
+
|
74
|
+
def does_not_match?(*__rest__)
|
75
|
+
raise "This matcher doesn't support negation"
|
76
|
+
end
|
77
|
+
|
78
|
+
def supports_block_expectations?() = true
|
79
|
+
|
80
|
+
def failure_message
|
81
|
+
"expected a scoping named :#{name} for type :#{type} " \
|
82
|
+
"#{scope_options_message} " \
|
83
|
+
"from #{policy} to have been applied, " \
|
84
|
+
"but #{actual_scopes_message}"
|
85
|
+
end
|
86
|
+
|
87
|
+
def scope_options_message
|
88
|
+
if scope_options
|
89
|
+
if defined?(::RSpec::Matchers::Composable) &&
|
90
|
+
scope_options.is_a?(::RSpec::Matchers::Composable)
|
91
|
+
"with scope options #{scope_options.description}"
|
92
|
+
else
|
93
|
+
"with scope options #{scope_options}"
|
94
|
+
end
|
95
|
+
else
|
96
|
+
"without scope options"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def actual_scopes_message
|
101
|
+
if actual_scopes.empty?
|
102
|
+
"no scopings have been made"
|
103
|
+
else
|
104
|
+
"the following scopings were encountered:\n" \
|
105
|
+
"#{formatted_scopings}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def formatted_scopings
|
110
|
+
actual_scopes.map do
|
111
|
+
" - #{_1.inspect}"
|
112
|
+
end.join("\n")
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
RSpec.configure do |config|
|
119
|
+
config.include(Module.new do
|
120
|
+
def have_authorized_scope(type)
|
121
|
+
ActionPolicy::RSpec::HaveAuthorizedScope.new(type)
|
122
|
+
end
|
123
|
+
end)
|
124
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
# Adds `suggest` method which uses did_you_mean
|
5
|
+
# to generate a suggestion message
|
6
|
+
module SuggestMessage
|
7
|
+
if defined?(::DidYouMean::SpellChecker)
|
8
|
+
def suggest(needle, heystack)
|
9
|
+
suggestion = ::DidYouMean::SpellChecker.new(
|
10
|
+
dictionary: heystack
|
11
|
+
).correct(needle).first
|
12
|
+
|
13
|
+
suggestion ? "\nDid you mean? #{suggestion}" : ""
|
14
|
+
end
|
15
|
+
else
|
16
|
+
def suggest(*__rest__) = ""
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -12,8 +12,10 @@ module ActionPolicy
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
-
# Register params scope matcher
|
16
|
-
ActionPolicy::Base.scope_matcher :action_controller_params, ActionController::Parameters
|
17
|
-
|
18
15
|
# Add alias to base policy
|
19
16
|
ActionPolicy::Base.extend ActionPolicy::ScopeMatchers::ActionControllerParams
|
17
|
+
|
18
|
+
ActiveSupport.on_load(:action_controller) do
|
19
|
+
# Register params scope matcher
|
20
|
+
ActionPolicy::Base.scope_matcher :action_controller_params, ActionController::Parameters
|
21
|
+
end
|
@@ -12,18 +12,20 @@ module ActionPolicy
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
-
# Register relation scope matcher
|
16
|
-
ActionPolicy::Base.scope_matcher :active_record_relation, ActiveRecord::Relation
|
17
|
-
|
18
15
|
# Add alias to base policy
|
19
16
|
ActionPolicy::Base.extend ActionPolicy::ScopeMatchers::ActiveRecord
|
20
17
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
18
|
+
ActiveSupport.on_load(:active_record) do
|
19
|
+
# Register relation scope matcher
|
20
|
+
ActionPolicy::Base.scope_matcher :active_record_relation, ActiveRecord::Relation
|
21
|
+
|
22
|
+
ActiveRecord::Relation.include(Module.new do
|
23
|
+
def policy_name
|
24
|
+
if model.respond_to?(:policy_name)
|
25
|
+
model.policy_name.to_s
|
26
|
+
else
|
27
|
+
"#{model}Policy"
|
28
|
+
end
|
27
29
|
end
|
28
|
-
end
|
29
|
-
end
|
30
|
+
end)
|
31
|
+
end
|
@@ -74,8 +74,6 @@ module ActionPolicy # :nodoc:
|
|
74
74
|
app.config.action_policy.namespace_cache_enabled
|
75
75
|
|
76
76
|
ActiveSupport.on_load(:action_controller) do
|
77
|
-
require "action_policy/rails/scope_matchers/action_controller_params"
|
78
|
-
|
79
77
|
next unless app.config.action_policy.auto_inject_into_controller
|
80
78
|
|
81
79
|
ActionController::Base.include ActionPolicy::Controller
|
@@ -95,21 +93,12 @@ module ActionPolicy # :nodoc:
|
|
95
93
|
ActionCable::Channel::Base.authorize :user, through: :current_user
|
96
94
|
end
|
97
95
|
|
96
|
+
# Scope matchers
|
97
|
+
require "action_policy/rails/scope_matchers/action_controller_params"
|
98
|
+
require "action_policy/rails/scope_matchers/active_record"
|
99
|
+
|
98
100
|
ActiveSupport.on_load(:active_record) do
|
99
101
|
require "action_policy/rails/ext/active_record"
|
100
|
-
require "action_policy/rails/scope_matchers/active_record"
|
101
|
-
end
|
102
|
-
|
103
|
-
# Trigger load hooks of the components that extend ActionPolicy itself
|
104
|
-
# (e.g., scope matchers)
|
105
|
-
begin
|
106
|
-
::ActionController::Base
|
107
|
-
rescue NameError
|
108
|
-
end
|
109
|
-
|
110
|
-
begin
|
111
|
-
::ActiveRecord::Base
|
112
|
-
rescue NameError
|
113
102
|
end
|
114
103
|
end
|
115
104
|
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.6.
|
4
|
+
version: 0.6.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vladimir Dementyev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-01-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ruby-next-core
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: '1.0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: '1.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: ammeter
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -162,6 +162,13 @@ files:
|
|
162
162
|
- lib/.rbnext/3.1/action_policy/ext/module_namespace.rb
|
163
163
|
- lib/.rbnext/3.1/action_policy/ext/policy_cache_key.rb
|
164
164
|
- lib/.rbnext/3.1/action_policy/policy/authorization.rb
|
165
|
+
- lib/.rbnext/3.2/action_policy/behaviours/policy_for.rb
|
166
|
+
- lib/.rbnext/3.2/action_policy/ext/policy_cache_key.rb
|
167
|
+
- lib/.rbnext/3.2/action_policy/lookup_chain.rb
|
168
|
+
- lib/.rbnext/3.2/action_policy/policy/core.rb
|
169
|
+
- lib/.rbnext/3.2/action_policy/rspec/be_authorized_to.rb
|
170
|
+
- lib/.rbnext/3.2/action_policy/rspec/have_authorized_scope.rb
|
171
|
+
- lib/.rbnext/3.2/action_policy/utils/suggest_message.rb
|
165
172
|
- lib/action_policy.rb
|
166
173
|
- lib/action_policy/authorizer.rb
|
167
174
|
- lib/action_policy/base.rb
|
@@ -242,7 +249,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
242
249
|
- !ruby/object:Gem::Version
|
243
250
|
version: '0'
|
244
251
|
requirements: []
|
245
|
-
rubygems_version: 3.4.
|
252
|
+
rubygems_version: 3.4.20
|
246
253
|
signing_key:
|
247
254
|
specification_version: 4
|
248
255
|
summary: Authorization framework for Ruby/Rails application
|