action_policy 0.0.1 → 0.1.0
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 +5 -5
- data/.rubocop.yml +85 -0
- data/.travis.yml +25 -2
- data/CHANGELOG.md +7 -0
- data/Gemfile +12 -3
- data/README.md +71 -12
- data/Rakefile +9 -1
- data/action_policy.gemspec +11 -5
- data/docs/.nojekyll +0 -0
- data/docs/CNAME +1 -0
- data/docs/README.md +46 -0
- data/docs/_sidebar.md +19 -0
- data/docs/aliases.md +54 -0
- data/docs/assets/docsify.min.js +1 -0
- data/docs/assets/fonts/FiraCode-Medium.woff +0 -0
- data/docs/assets/fonts/FiraCode-Regular.woff +0 -0
- data/docs/assets/images/cache.png +0 -0
- data/docs/assets/images/cache.svg +70 -0
- data/docs/assets/images/layer.png +0 -0
- data/docs/assets/images/layer.svg +92 -0
- data/docs/assets/prism-ruby.min.js +1 -0
- data/docs/assets/styles.css +317 -0
- data/docs/assets/vue.min.css +1 -0
- data/docs/authorization_context.md +33 -0
- data/docs/caching.md +262 -0
- data/docs/custom_lookup_chain.md +48 -0
- data/docs/custom_policy.md +51 -0
- data/docs/favicon.ico +0 -0
- data/docs/i18n.md +3 -0
- data/docs/index.html +25 -0
- data/docs/instrumentation.md +3 -0
- data/docs/lookup_chain.md +16 -0
- data/docs/namespaces.md +69 -0
- data/docs/non_rails.md +29 -0
- data/docs/pre_checks.md +57 -0
- data/docs/quick_start.md +102 -0
- data/docs/rails.md +110 -0
- data/docs/reasons.md +67 -0
- data/docs/testing.md +116 -0
- data/docs/writing_policies.md +55 -0
- data/gemfiles/jruby.gemfile +5 -0
- data/gemfiles/rails42.gemfile +5 -0
- data/gemfiles/railsmaster.gemfile +6 -0
- data/lib/action_policy.rb +34 -2
- data/lib/action_policy/authorizer.rb +28 -0
- data/lib/action_policy/base.rb +24 -0
- data/lib/action_policy/behaviour.rb +94 -0
- data/lib/action_policy/behaviours/memoized.rb +56 -0
- data/lib/action_policy/behaviours/namespaced.rb +80 -0
- data/lib/action_policy/behaviours/policy_for.rb +23 -0
- data/lib/action_policy/behaviours/thread_memoized.rb +54 -0
- data/lib/action_policy/ext/module_namespace.rb +21 -0
- data/lib/action_policy/ext/policy_cache_key.rb +67 -0
- data/lib/action_policy/ext/string_constantize.rb +23 -0
- data/lib/action_policy/lookup_chain.rb +84 -0
- data/lib/action_policy/policy/aliases.rb +69 -0
- data/lib/action_policy/policy/authorization.rb +91 -0
- data/lib/action_policy/policy/cache.rb +74 -0
- data/lib/action_policy/policy/cached_apply.rb +28 -0
- data/lib/action_policy/policy/core.rb +64 -0
- data/lib/action_policy/policy/defaults.rb +37 -0
- data/lib/action_policy/policy/pre_check.rb +210 -0
- data/lib/action_policy/policy/reasons.rb +109 -0
- data/lib/action_policy/rails/channel.rb +15 -0
- data/lib/action_policy/rails/controller.rb +90 -0
- data/lib/action_policy/railtie.rb +74 -0
- data/lib/action_policy/rspec.rb +3 -0
- data/lib/action_policy/rspec/be_authorized_to.rb +93 -0
- data/lib/action_policy/rspec/pundit_syntax.rb +48 -0
- data/lib/action_policy/test_helper.rb +46 -0
- data/lib/action_policy/testing.rb +64 -0
- data/lib/action_policy/version.rb +3 -1
- metadata +115 -9
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
module Ext
|
5
|
+
# Add Module#namespace method
|
6
|
+
module ModuleNamespace
|
7
|
+
refine Module do
|
8
|
+
unless "".respond_to?(:safe_constantize)
|
9
|
+
require "action_policy/ext/string_constantize"
|
10
|
+
using ActionPolicy::Ext::StringConstantize
|
11
|
+
end
|
12
|
+
|
13
|
+
def namespace
|
14
|
+
return unless name.match?(/[^^]::/)
|
15
|
+
|
16
|
+
name.sub(/::[^:]+$/, "").safe_constantize
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,67 @@
|
|
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
|
13
|
+
refine Object do
|
14
|
+
def _policy_cache_key(use_object_id: false)
|
15
|
+
return policy_cache_key if respond_to?(:policy_cache_key)
|
16
|
+
return cache_key if respond_to?(:cache_key)
|
17
|
+
|
18
|
+
return object_id if use_object_id == true
|
19
|
+
|
20
|
+
raise ArgumentError, "object is not cacheable"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
refine NilClass do
|
25
|
+
def _policy_cache_key(*)
|
26
|
+
""
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
refine TrueClass do
|
31
|
+
def _policy_cache_key(*)
|
32
|
+
"t"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
refine FalseClass do
|
37
|
+
def _policy_cache_key(*)
|
38
|
+
"f"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
refine String do
|
43
|
+
def _policy_cache_key(*)
|
44
|
+
self
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
refine Symbol do
|
49
|
+
def _policy_cache_key(*)
|
50
|
+
to_s
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
refine Numeric do
|
55
|
+
def _policy_cache_key(*)
|
56
|
+
to_s
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
refine Time do
|
61
|
+
def _policy_cache_key(*)
|
62
|
+
to_s
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
module Ext
|
5
|
+
# Add simple safe_constantize method to String
|
6
|
+
module StringConstantize
|
7
|
+
refine String do
|
8
|
+
def safe_constantize
|
9
|
+
names = split("::")
|
10
|
+
|
11
|
+
return nil if names.empty?
|
12
|
+
|
13
|
+
# Remove the first blank element in case of '::ClassName' notation.
|
14
|
+
names.shift if names.size > 1 && names.first.empty?
|
15
|
+
|
16
|
+
names.inject(Object) do |constant, name|
|
17
|
+
constant.const_get(name) if constant.const_defined?(name)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,84 @@
|
|
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/module_namespace"
|
16
|
+
using ActionPolicy::Ext::ModuleNamespace
|
17
|
+
|
18
|
+
class << self
|
19
|
+
attr_accessor :chain
|
20
|
+
|
21
|
+
def call(record, **opts)
|
22
|
+
chain.each do |probe|
|
23
|
+
val = probe.call(record, **opts)
|
24
|
+
return val unless val.nil?
|
25
|
+
end
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def lookup_within_namespace(record, namespace)
|
32
|
+
policy_name = policy_class_name_for(record)
|
33
|
+
mod = namespace
|
34
|
+
loop do
|
35
|
+
return mod.const_get(policy_name, false) if
|
36
|
+
mod.const_defined?(policy_name, false)
|
37
|
+
|
38
|
+
mod = mod.namespace
|
39
|
+
|
40
|
+
return if mod.nil?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def policy_class_name_for(record)
|
45
|
+
record_class = record.is_a?(Class) ? record : record.class
|
46
|
+
|
47
|
+
if record_class.respond_to?(:policy_name)
|
48
|
+
record_class.policy_name.to_s
|
49
|
+
else
|
50
|
+
"#{record_class}Policy"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# By self `policy_class` method
|
56
|
+
INSTANCE_POLICY_CLASS = ->(record, _) {
|
57
|
+
record.policy_class if record.respond_to?(:policy_class)
|
58
|
+
}
|
59
|
+
|
60
|
+
# By record's class `policy_class` method
|
61
|
+
CLASS_POLICY_CLASS = ->(record, _) {
|
62
|
+
record.class.policy_class if record.class.respond_to?(:policy_class)
|
63
|
+
}
|
64
|
+
|
65
|
+
# Lookup within namespace when provided
|
66
|
+
NAMESPACE_LOOKUP = ->(record, namespace: nil, **) {
|
67
|
+
next if namespace.nil?
|
68
|
+
|
69
|
+
lookup_within_namespace(record, namespace)
|
70
|
+
}
|
71
|
+
|
72
|
+
# Infer from class name
|
73
|
+
INFER_FROM_CLASS = ->(record, _) {
|
74
|
+
policy_class_name_for(record).safe_constantize
|
75
|
+
}
|
76
|
+
|
77
|
+
self.chain = [
|
78
|
+
INSTANCE_POLICY_CLASS,
|
79
|
+
CLASS_POLICY_CLASS,
|
80
|
+
NAMESPACE_LOOKUP,
|
81
|
+
INFER_FROM_CLASS
|
82
|
+
]
|
83
|
+
end
|
84
|
+
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
|
+
# prepend 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 prepended(base)
|
27
|
+
base.extend ClassMethods
|
28
|
+
base.prepend InstanceMethods
|
29
|
+
end
|
30
|
+
|
31
|
+
alias included prepended
|
32
|
+
end
|
33
|
+
|
34
|
+
module InstanceMethods # :nodoc:
|
35
|
+
def resolve_rule(activity)
|
36
|
+
return activity if respond_to?(activity)
|
37
|
+
self.class.lookup_alias(activity) || super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module ClassMethods # :nodoc:
|
42
|
+
def default_rule(val)
|
43
|
+
rules_aliases[DEFAULT] = val
|
44
|
+
end
|
45
|
+
|
46
|
+
def alias_rule(*rules, to:)
|
47
|
+
rules.each do |rule|
|
48
|
+
rules_aliases[rule] = to
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def lookup_alias(rule)
|
53
|
+
rules_aliases.fetch(rule, rules_aliases[DEFAULT])
|
54
|
+
end
|
55
|
+
|
56
|
+
def rules_aliases
|
57
|
+
return @rules_aliases if instance_variable_defined?(:@rules_aliases)
|
58
|
+
|
59
|
+
@rules_aliases =
|
60
|
+
if superclass.respond_to?(:rules_aliases)
|
61
|
+
superclass.rules_aliases.dup
|
62
|
+
else
|
63
|
+
{}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,91 @@
|
|
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 prepended(base)
|
40
|
+
base.extend ClassMethods
|
41
|
+
base.prepend InstanceMethods
|
42
|
+
end
|
43
|
+
|
44
|
+
alias included prepended
|
45
|
+
end
|
46
|
+
|
47
|
+
attr_reader :authorization_context
|
48
|
+
|
49
|
+
module InstanceMethods # :nodoc:
|
50
|
+
def initialize(*args, **params)
|
51
|
+
super(*args)
|
52
|
+
|
53
|
+
@authorization_context = {}
|
54
|
+
|
55
|
+
self.class.authorization_targets.each do |id, opts|
|
56
|
+
raise AuthorizationContextMissing, id unless params.key?(id)
|
57
|
+
|
58
|
+
val = params.fetch(id)
|
59
|
+
|
60
|
+
raise AuthorizationContextMissing, id if val.nil? && opts[:allow_nil] != true
|
61
|
+
|
62
|
+
authorization_context[id] = instance_variable_set("@#{id}", val)
|
63
|
+
end
|
64
|
+
|
65
|
+
authorization_context.freeze
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
module ClassMethods # :nodoc:
|
70
|
+
def authorize(*ids, **opts)
|
71
|
+
ids.each do |id|
|
72
|
+
authorization_targets[id] = opts
|
73
|
+
end
|
74
|
+
|
75
|
+
attr_reader(*ids)
|
76
|
+
end
|
77
|
+
|
78
|
+
def authorization_targets
|
79
|
+
return @authorization_targets if instance_variable_defined?(:@authorization_targets)
|
80
|
+
|
81
|
+
@authorization_targets =
|
82
|
+
if superclass.respond_to?(:authorization_targets)
|
83
|
+
superclass.authorization_targets.dup
|
84
|
+
else
|
85
|
+
{}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_policy/version"
|
4
|
+
|
5
|
+
module ActionPolicy # :nodoc:
|
6
|
+
# By default cache namespace (or prefix) contains major and minor version of the gem
|
7
|
+
CACHE_NAMESPACE = "acp:#{ActionPolicy::VERSION.split('.').take(2).join('.')}"
|
8
|
+
|
9
|
+
require "action_policy/ext/policy_cache_key"
|
10
|
+
using ActionPolicy::Ext::PolicyCacheKey
|
11
|
+
|
12
|
+
module Policy
|
13
|
+
# Provides long-lived cache through ActionPolicy.cache_store.
|
14
|
+
#
|
15
|
+
# NOTE: if cache_store is nil then we silently skip all the caching.
|
16
|
+
module Cache
|
17
|
+
class << self
|
18
|
+
def prepended(base)
|
19
|
+
base.extend ClassMethods
|
20
|
+
base.prepend InstanceMethods
|
21
|
+
end
|
22
|
+
|
23
|
+
alias included prepended
|
24
|
+
end
|
25
|
+
|
26
|
+
def cache_namespace
|
27
|
+
ActionPolicy::CACHE_NAMESPACE
|
28
|
+
end
|
29
|
+
|
30
|
+
def cache_key(rule)
|
31
|
+
"#{cache_namespace}/#{context_cache_key}/" \
|
32
|
+
"#{record._policy_cache_key}/#{self.class.name}/#{rule}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def context_cache_key
|
36
|
+
authorization_context.map { |_k, v| v._policy_cache_key.to_s }.join("/")
|
37
|
+
end
|
38
|
+
|
39
|
+
def apply_with_cache(rule)
|
40
|
+
options = self.class.cached_rules.fetch(rule)
|
41
|
+
|
42
|
+
ActionPolicy.cache_store.fetch(cache_key(rule), options) { yield }
|
43
|
+
end
|
44
|
+
|
45
|
+
module InstanceMethods # :nodoc:
|
46
|
+
def apply(rule)
|
47
|
+
return super if ActionPolicy.cache_store.nil? ||
|
48
|
+
!self.class.cached_rules.key?(rule)
|
49
|
+
|
50
|
+
apply_with_cache(rule) { super }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
module ClassMethods # :nodoc:
|
55
|
+
def cache(*rules, **options)
|
56
|
+
rules.each do |rule|
|
57
|
+
cached_rules[rule] = options
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def cached_rules
|
62
|
+
return @cached_rules if instance_variable_defined?(:@cached_rules)
|
63
|
+
|
64
|
+
@cached_rules =
|
65
|
+
if superclass.respond_to?(:cached_rules)
|
66
|
+
superclass.cached_rules.dup
|
67
|
+
else
|
68
|
+
{}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
module Policy
|
5
|
+
# Per-policy cache for applied rules.
|
6
|
+
#
|
7
|
+
# When you call `apply` twice on the same policy and for the same rule,
|
8
|
+
# the check (and pre-checks) is only called once.
|
9
|
+
module CachedApply
|
10
|
+
class << self
|
11
|
+
def prepended(base)
|
12
|
+
base.prepend InstanceMethods
|
13
|
+
end
|
14
|
+
|
15
|
+
alias included prepended
|
16
|
+
end
|
17
|
+
|
18
|
+
module InstanceMethods # :nodoc:
|
19
|
+
def apply(rule)
|
20
|
+
@__rules_cache__ ||= {}
|
21
|
+
return @__rules_cache__[rule] if @__rules_cache__.key?(rule)
|
22
|
+
|
23
|
+
@__rules_cache__[rule] = super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|