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.
Files changed (73) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +85 -0
  3. data/.travis.yml +25 -2
  4. data/CHANGELOG.md +7 -0
  5. data/Gemfile +12 -3
  6. data/README.md +71 -12
  7. data/Rakefile +9 -1
  8. data/action_policy.gemspec +11 -5
  9. data/docs/.nojekyll +0 -0
  10. data/docs/CNAME +1 -0
  11. data/docs/README.md +46 -0
  12. data/docs/_sidebar.md +19 -0
  13. data/docs/aliases.md +54 -0
  14. data/docs/assets/docsify.min.js +1 -0
  15. data/docs/assets/fonts/FiraCode-Medium.woff +0 -0
  16. data/docs/assets/fonts/FiraCode-Regular.woff +0 -0
  17. data/docs/assets/images/cache.png +0 -0
  18. data/docs/assets/images/cache.svg +70 -0
  19. data/docs/assets/images/layer.png +0 -0
  20. data/docs/assets/images/layer.svg +92 -0
  21. data/docs/assets/prism-ruby.min.js +1 -0
  22. data/docs/assets/styles.css +317 -0
  23. data/docs/assets/vue.min.css +1 -0
  24. data/docs/authorization_context.md +33 -0
  25. data/docs/caching.md +262 -0
  26. data/docs/custom_lookup_chain.md +48 -0
  27. data/docs/custom_policy.md +51 -0
  28. data/docs/favicon.ico +0 -0
  29. data/docs/i18n.md +3 -0
  30. data/docs/index.html +25 -0
  31. data/docs/instrumentation.md +3 -0
  32. data/docs/lookup_chain.md +16 -0
  33. data/docs/namespaces.md +69 -0
  34. data/docs/non_rails.md +29 -0
  35. data/docs/pre_checks.md +57 -0
  36. data/docs/quick_start.md +102 -0
  37. data/docs/rails.md +110 -0
  38. data/docs/reasons.md +67 -0
  39. data/docs/testing.md +116 -0
  40. data/docs/writing_policies.md +55 -0
  41. data/gemfiles/jruby.gemfile +5 -0
  42. data/gemfiles/rails42.gemfile +5 -0
  43. data/gemfiles/railsmaster.gemfile +6 -0
  44. data/lib/action_policy.rb +34 -2
  45. data/lib/action_policy/authorizer.rb +28 -0
  46. data/lib/action_policy/base.rb +24 -0
  47. data/lib/action_policy/behaviour.rb +94 -0
  48. data/lib/action_policy/behaviours/memoized.rb +56 -0
  49. data/lib/action_policy/behaviours/namespaced.rb +80 -0
  50. data/lib/action_policy/behaviours/policy_for.rb +23 -0
  51. data/lib/action_policy/behaviours/thread_memoized.rb +54 -0
  52. data/lib/action_policy/ext/module_namespace.rb +21 -0
  53. data/lib/action_policy/ext/policy_cache_key.rb +67 -0
  54. data/lib/action_policy/ext/string_constantize.rb +23 -0
  55. data/lib/action_policy/lookup_chain.rb +84 -0
  56. data/lib/action_policy/policy/aliases.rb +69 -0
  57. data/lib/action_policy/policy/authorization.rb +91 -0
  58. data/lib/action_policy/policy/cache.rb +74 -0
  59. data/lib/action_policy/policy/cached_apply.rb +28 -0
  60. data/lib/action_policy/policy/core.rb +64 -0
  61. data/lib/action_policy/policy/defaults.rb +37 -0
  62. data/lib/action_policy/policy/pre_check.rb +210 -0
  63. data/lib/action_policy/policy/reasons.rb +109 -0
  64. data/lib/action_policy/rails/channel.rb +15 -0
  65. data/lib/action_policy/rails/controller.rb +90 -0
  66. data/lib/action_policy/railtie.rb +74 -0
  67. data/lib/action_policy/rspec.rb +3 -0
  68. data/lib/action_policy/rspec/be_authorized_to.rb +93 -0
  69. data/lib/action_policy/rspec/pundit_syntax.rb +48 -0
  70. data/lib/action_policy/test_helper.rb +46 -0
  71. data/lib/action_policy/testing.rb +64 -0
  72. data/lib/action_policy/version.rb +3 -1
  73. 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