action_policy 0.0.1 → 0.1.0

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