action_policy 0.2.4 → 0.3.0.beta1

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +26 -64
  3. data/.travis.yml +13 -10
  4. data/CHANGELOG.md +216 -1
  5. data/Gemfile +7 -0
  6. data/LICENSE.txt +1 -1
  7. data/Rakefile +10 -0
  8. data/action_policy.gemspec +5 -3
  9. data/benchmarks/namespaced_lookup_cache.rb +18 -22
  10. data/docs/README.md +3 -3
  11. data/docs/_sidebar.md +4 -0
  12. data/docs/aliases.md +9 -5
  13. data/docs/authorization_context.md +59 -1
  14. data/docs/behaviour.md +113 -0
  15. data/docs/caching.md +6 -4
  16. data/docs/custom_policy.md +1 -2
  17. data/docs/debugging.md +55 -0
  18. data/docs/decorators.md +27 -0
  19. data/docs/i18n.md +41 -2
  20. data/docs/instrumentation.md +70 -2
  21. data/docs/lookup_chain.md +5 -4
  22. data/docs/namespaces.md +1 -1
  23. data/docs/non_rails.md +2 -3
  24. data/docs/pundit_migration.md +77 -2
  25. data/docs/quick_start.md +5 -5
  26. data/docs/rails.md +5 -2
  27. data/docs/reasons.md +50 -3
  28. data/docs/scoping.md +262 -0
  29. data/docs/testing.md +232 -21
  30. data/docs/writing_policies.md +1 -1
  31. data/gemfiles/jruby.gemfile +3 -0
  32. data/gemfiles/rails42.gemfile +3 -0
  33. data/gemfiles/rails6.gemfile +8 -0
  34. data/gemfiles/railsmaster.gemfile +1 -1
  35. data/lib/action_policy.rb +3 -3
  36. data/lib/action_policy/authorizer.rb +12 -4
  37. data/lib/action_policy/base.rb +2 -0
  38. data/lib/action_policy/behaviour.rb +14 -3
  39. data/lib/action_policy/behaviours/memoized.rb +1 -1
  40. data/lib/action_policy/behaviours/policy_for.rb +12 -3
  41. data/lib/action_policy/behaviours/scoping.rb +32 -0
  42. data/lib/action_policy/behaviours/thread_memoized.rb +1 -1
  43. data/lib/action_policy/ext/hash_transform_keys.rb +19 -0
  44. data/lib/action_policy/ext/module_namespace.rb +1 -1
  45. data/lib/action_policy/ext/policy_cache_key.rb +2 -1
  46. data/lib/action_policy/ext/proc_case_eq.rb +14 -0
  47. data/lib/action_policy/ext/string_constantize.rb +1 -0
  48. data/lib/action_policy/ext/symbol_classify.rb +22 -0
  49. data/lib/action_policy/i18n.rb +56 -0
  50. data/lib/action_policy/lookup_chain.rb +21 -3
  51. data/lib/action_policy/policy/cache.rb +10 -6
  52. data/lib/action_policy/policy/core.rb +31 -19
  53. data/lib/action_policy/policy/execution_result.rb +12 -0
  54. data/lib/action_policy/policy/pre_check.rb +2 -6
  55. data/lib/action_policy/policy/reasons.rb +99 -12
  56. data/lib/action_policy/policy/scoping.rb +165 -0
  57. data/lib/action_policy/rails/authorizer.rb +20 -0
  58. data/lib/action_policy/rails/controller.rb +4 -14
  59. data/lib/action_policy/rails/ext/active_record.rb +10 -0
  60. data/lib/action_policy/rails/policy/instrumentation.rb +24 -0
  61. data/lib/action_policy/rails/scope_matchers/action_controller_params.rb +19 -0
  62. data/lib/action_policy/rails/scope_matchers/active_record.rb +29 -0
  63. data/lib/action_policy/railtie.rb +29 -7
  64. data/lib/action_policy/rspec.rb +1 -0
  65. data/lib/action_policy/rspec/be_authorized_to.rb +1 -1
  66. data/lib/action_policy/rspec/dsl.rb +103 -0
  67. data/lib/action_policy/rspec/have_authorized_scope.rb +126 -0
  68. data/lib/action_policy/rspec/pundit_syntax.rb +1 -1
  69. data/lib/action_policy/test_helper.rb +69 -4
  70. data/lib/action_policy/testing.rb +54 -0
  71. data/lib/action_policy/utils/pretty_print.rb +137 -0
  72. data/lib/action_policy/utils/suggest_message.rb +21 -0
  73. data/lib/action_policy/version.rb +1 -1
  74. metadata +58 -11
@@ -1,17 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- =begin
4
-
5
- This benchmark measures the efficiency of NamespaceCache.
6
-
7
- Run it multiple times with cache on/off to see the results:
8
-
9
- $ bundle exec ruby namespaced_lookup_cache.rb
10
- $ bundle exec ruby namespaced_lookup_cache.rb
11
- $ NO_CACHE=1 bundle exec ruby namespaced_lookup_cache.rb
12
- $ NO_CACHE=1 bundle exec ruby namespaced_lookup_cache.rb
13
-
14
- =end
3
+ #
4
+ # This benchmark measures the efficiency of NamespaceCache.
5
+ #
6
+ # Run it multiple times with cache on/off to see the results:
7
+ #
8
+ # $ bundle exec ruby namespaced_lookup_cache.rb
9
+ # $ bundle exec ruby namespaced_lookup_cache.rb
10
+ # $ NO_CACHE=1 bundle exec ruby namespaced_lookup_cache.rb
11
+ # $ NO_CACHE=1 bundle exec ruby namespaced_lookup_cache.rb
12
+ #
15
13
 
16
14
  $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
17
15
 
@@ -37,7 +35,7 @@ end
37
35
  a = A.new
38
36
  b = B.new
39
37
 
40
- if ENV['NO_CACHE']
38
+ if ENV["NO_CACHE"]
41
39
  ActionPolicy::LookupChain.namespace_cache_enabled = false
42
40
  end
43
41
 
@@ -60,16 +58,14 @@ Benchmark.ips do |x|
60
58
  ActionPolicy.lookup(b, namespace: X::Y::Z)
61
59
  end
62
60
 
63
- x.hold! 'temp_results'
61
+ x.hold! "temp_results"
64
62
 
65
63
  x.compare!
66
64
  end
67
65
 
68
- =begin
69
-
70
- Comparison:
71
- cache B: 178577.4 i/s
72
- cache A: 173061.4 i/s - same-ish: difference falls within error
73
- no cache A: 97991.7 i/s - same-ish: difference falls within error
74
- no cache B: 42505.4 i/s - 4.20x slower
75
- =end
66
+ #
67
+ # Comparison:
68
+ # cache B: 178577.4 i/s
69
+ # cache A: 173061.4 i/s - same-ish: difference falls within error
70
+ # no cache A: 97991.7 i/s - same-ish: difference falls within error
71
+ # no cache B: 42505.4 i/s - 4.20x slower
@@ -36,11 +36,11 @@ Why did we decide to build our own authorization gem instead of using the existi
36
36
 
37
37
  [Pundit][] has been our framework of choice for a long time. Being too _dead-simple_, it required a lot of hacking to fulfill business logic requirements.
38
38
 
39
- These _hacks_ later become into Action Policy (initially, we even called it "Pundit, re-visited").
39
+ These _hacks_ later became Action Policy (initially, we even called it "Pundit, re-visited").
40
40
 
41
41
  We also took a few ideas from [CanCanCan][]—such as [default rules and rule aliases](./aliases.md).
42
42
 
43
- It is also worth noting that Action Policy (despite from a _Railsy_ name) is designed to be **Rails-free**. On the other hand, it contains some Rails-specific extensions and seamlessly integrates into the framework.
43
+ It is also worth noting that Action Policy (despite having a _Railsy_ name) is designed to be **Rails-free**. On the other hand, it contains some Rails-specific extensions and seamlessly integrates into the framework.
44
44
 
45
45
  So, what are the main reasons to consider Action Policy as your authorization tool?
46
46
 
@@ -50,7 +50,7 @@ So, what are the main reasons to consider Action Policy as your authorization to
50
50
 
51
51
  - **Code Organization**: use [namespaces](./namespaces.md) to organize your policies (for example, when you have multiple authorization strategies); add [pre-checks](./pre_checks.md) to make rules more readable and better express your business-logic.
52
52
 
53
- - ...and more: [testability](./testing.md), [i18n](./i18n.md) integrations, [actionable errors](./reasons.md).
53
+ - **...and more**: [testability](./testing.md), [i18n](./i18n.md) integrations, [actionable errors](./reasons.md).
54
54
 
55
55
  Learn more about the motivation behind the Action Policy and its features by watching this [RailsConf talk](https://www.youtube.com/watch?v=NVwx0DARDis).
56
56
 
@@ -5,17 +5,21 @@
5
5
  * [Non-Rails Usage](non_rails.md)
6
6
  * [Testing](testing.md)
7
7
  * Features
8
+ * [Authorization Behaviour](behaviour.md)
8
9
  * [Policy Lookup](lookup_chain.md)
9
10
  * [Authorization Context](authorization_context.md)
10
11
  * [Aliases](aliases.md)
11
12
  * [Pre-Checks](pre_checks.md)
13
+ * [Scoping](scoping.md)
12
14
  * [Caching](caching.md)
13
15
  * [Namespaces](namespaces.md)
14
16
  * [Failure Reasons](reasons.md)
15
17
  * [Instrumentation](instrumentation.md)
16
18
  * [I18n Support](i18n.md)
19
+ * [Debugging](debugging.md)
17
20
  * Tips & Tricks
18
21
  * [From Pundit to Action Policy](./pundit_migration.md)
22
+ * [Dealing with Decorators](./decorators.md)
19
23
  * [Controller Action Aliases](controller_action_aliases.md)
20
24
  * Customize
21
25
  * [Base Policy](custom_policy.md)
@@ -76,11 +76,14 @@ class SuperPolicy < ApplicationPolicy
76
76
 
77
77
  alias_rule :update?, :destroy?, :create?, to: :edit?
78
78
 
79
- def manage?; end
79
+ def manage?
80
+ end
80
81
 
81
- def edit?; end
82
+ def edit?
83
+ end
82
84
 
83
- def index?; end
85
+ def index?
86
+ end
84
87
  end
85
88
 
86
89
  class SubPolicy < AbstractPolicy
@@ -88,7 +91,8 @@ class SubPolicy < AbstractPolicy
88
91
 
89
92
  alias_rule :index?, :update?, to: :manage?
90
93
 
91
- def create?; end
94
+ def create?
95
+ end
92
96
  end
93
97
  ```
94
98
 
@@ -102,7 +106,7 @@ Authorizing against the SuperPolicy:
102
106
  * `index?` will resolve to `index?`
103
107
  * `something?` will resolve to `manage?`
104
108
 
105
- Authorizing against the SuBPolicy:
109
+ Authorizing against the SubPolicy:
106
110
 
107
111
  * `index?` will resolve to `manage?`
108
112
  * `update?` will resolve to `manage?`
@@ -8,7 +8,7 @@ You must configure authorization context in **two places**: in the policy itself
8
8
 
9
9
  By default, `ActionPolicy::Base` includes `user` as authorization context. If you don't need it, you have to [build your own base policy](custom_policy.md).
10
10
 
11
- To specify additional contexts, you should use `authorize` method:
11
+ To specify additional contexts, you should use the `authorize` method:
12
12
 
13
13
  ```ruby
14
14
  class ApplicationPolicy < ActionPolicy::Base
@@ -31,3 +31,61 @@ class ApplicationController < ActionController::Base
31
31
  authorize :account, through: :current_account
32
32
  end
33
33
  ```
34
+
35
+ ## Nested Policies vs Contexts
36
+
37
+ See also: [action_policy#36](https://github.com/palkan/action_policy/issues/36) and [action_policy#37](https://github.com/palkan/action_policy/pull/37)
38
+
39
+ When you call another policy from the policy object (e.g. via `allowed_to?` method),
40
+ the context of the current policy is passed to the _nested_ policy.
41
+
42
+ That means that if the nested policy has a different authorization context, we won't be able
43
+ to build it (event if you configure all the required keys in the controller).
44
+
45
+ For example:
46
+
47
+ ```ruby
48
+ class UserPolicy < ActionPolicy::Base
49
+ authorize :user
50
+
51
+ def show?
52
+ allowed_to?(:show?, record.profile)
53
+ end
54
+ end
55
+
56
+ class ProfilePolicy < ActionPolicy::Base
57
+ authorize :user, :account
58
+ end
59
+
60
+ class ApplicationController < ActionController::Base
61
+ authorize :user, through: :current_user
62
+ authorize :account, through: :current_account
63
+ end
64
+
65
+ class UsersController < ApplicationController
66
+ def show
67
+ user = User.find(params[:id])
68
+
69
+ authorize! user #=> raises "Missing policy authorization context: account"
70
+ end
71
+ end
72
+ ```
73
+
74
+ That means that **all the policies that could be used together MUST share the same set of authorization contexts** (or at least the _parent_ policies contexts must be subsets of the nested policies contexts).
75
+
76
+
77
+ ## Explicit context
78
+
79
+ You can override the _implicit_ authorization context (generated with `authorize` method) in-place
80
+ by passing the `context` option:
81
+
82
+ ```ruby
83
+ def show
84
+ user = User.find(params[:id])
85
+
86
+ authorize! user, context: {account: user.account}
87
+ end
88
+ ```
89
+
90
+ **NOTE:** the explictly provided context is merged with the implicit one (i.e. you can specify
91
+ only the keys you want to override).
@@ -0,0 +1,113 @@
1
+ # Action Policy Behaviour
2
+
3
+ Action Policy provides a mixin called `ActionPolicy::Behaviour` which adds authorization methods to your classes.
4
+
5
+ ## Usage
6
+
7
+ Let's make our custom _service_ object aware of authorization:
8
+
9
+ ```ruby
10
+ class PostUpdateAction
11
+ # First, we should include the behaviour
12
+ include ActionPolicy::Behaviour
13
+
14
+ # Secondly, provide authorization subject (performer)
15
+ authorize :user
16
+
17
+ attr_reader :user
18
+
19
+ def initialize(user)
20
+ @user = user
21
+ end
22
+
23
+ def call(post, params)
24
+ # Now we can use authorization methods
25
+ authorize! post, to: :update?
26
+
27
+ post.update!(params)
28
+ end
29
+ end
30
+ ```
31
+
32
+ `ActionPolicy::Behaviour` provides `authorize` class-level method to configure [authorization context](authorization_context.md) and the instance-level methods: `authorize!`, `allowed_to?` and `authorized`:
33
+
34
+ ### `authorize!`
35
+
36
+ This is a _guard-method_ which raises an `ActionPolicy::Unauthorized` exception
37
+ if authorization failed (i.e. policy rule returns false):
38
+
39
+ ```ruby
40
+ # `to` is a name of the policy rule to apply
41
+ authorize! post, to: :update?
42
+ ```
43
+
44
+ ### `allowed_to?`
45
+
46
+ This is a _predicate_ version of `authorize!`: it returns true if authorization succeed and false otherwise:
47
+
48
+ ```ruby
49
+ # the first argument is the rule to apply
50
+ # the second one is the target
51
+ if allowed_to?(:edit?, post)
52
+ # ...
53
+ end
54
+ ```
55
+
56
+ ### `authorized`
57
+
58
+ See [scoping](./scoping.md) docs.
59
+
60
+ ## Policy lookup
61
+
62
+ All three instance methods (`authorize!`, `allowed_to?`, `authorized`) uses the same
63
+ `policy_for` to lookup a policy class for authorization target. So, you can provide additional options to control the policy lookup process:
64
+
65
+ - Explicitly specify policy class using `with` option:
66
+
67
+ ```ruby
68
+ allowed_to?(:edit?, post, with: SpecialPostPolicy)
69
+ ```
70
+
71
+ - Provide a [namespace](./namespaces.md):
72
+
73
+ ```ruby
74
+ # Would try to lookup Admin::PostPolicy first
75
+ authorize! post, to: :destroy?, namespace: Admin
76
+ ```
77
+
78
+ ## Implicit authorization target
79
+
80
+ You can omit the authorization target for all the methods by defining an _implicit authorization target_:
81
+
82
+ ```ruby
83
+ class PostActions
84
+ include ActionPolicy::Behaviour
85
+
86
+ authorize :user
87
+
88
+ attr_reader :user, :post
89
+
90
+ def initialize(user, post)
91
+ @user = user
92
+ @post = post
93
+ end
94
+
95
+ def update(params)
96
+ # post is used here implicitly as a target
97
+ authorize! to: :update
98
+
99
+ post.update!(params)
100
+ end
101
+
102
+ def destroy
103
+ # post is used here implicitly as a target
104
+ authorize! to: :destroy
105
+
106
+ post.destroy!
107
+ end
108
+
109
+ def implicit_authorization_target
110
+ post
111
+ end
112
+ end
113
+ ```
@@ -139,10 +139,10 @@ class StagePolicy < ApplicationPolicy
139
139
  def full_access?
140
140
  !record.funnel.is_private? ||
141
141
  user.permissions
142
- .where(
143
- funnel_id: record.funnel_id,
144
- full_access: true
145
- ).exists?
142
+ .where(
143
+ funnel_id: record.funnel_id,
144
+ full_access: true
145
+ ).exists?
146
146
  end
147
147
  end
148
148
  ```
@@ -177,6 +177,8 @@ Where `cache_namespace` is equal to `"acp:#{MAJOR_GEM_VERSION}.#{MINOR_GEM_VERSI
177
177
 
178
178
  If any object does not respond to `#policy_cache_key`, we fallback to `#cache_key`. If `#cache_key` is not defined, an `ArgumentError` is raised.
179
179
 
180
+ **NOTE:** if your `#cache_key` method is performance-heavy (e.g. like the `ActiveRecord::Relation`'s one), we recommend to explicitly define the `#policy_cache_key` method on the corresponding class to avoid unnecessary load. See also [action_policy#55](https://github.com/palkan/action_policy/issues/55).
181
+
180
182
  You can define your own `cache_key` / `cache_namespace` / `context_cache_key` methods for policy class to override this logic.
181
183
 
182
184
  #### Invalidation
@@ -4,7 +4,6 @@
4
4
 
5
5
  It looks like this:
6
6
 
7
- <span style="display:none;"># rubocop:disable Style/ClassAndModuleChildren</span>
8
7
 
9
8
  ```ruby
10
9
  class ActionPolicy::Base
@@ -38,7 +37,7 @@ class ActionPolicy::Base
38
37
  end
39
38
  ```
40
39
 
41
- <span style="display:none;"># rubocop:enable Style/ClassAndModuleChildren</span>
40
+
42
41
 
43
42
  You can write your `ApplicationPolicy` from scratch instead of inheriting from `ActionPolicy::Base`
44
43
  if the defaults above do not fit your needs. The only required component is `ActionPolicy::Policy::Core`:
@@ -0,0 +1,55 @@
1
+ # Debug Helpers
2
+
3
+ **NOTE:** this functionality requires two additional gems to be available in the app:
4
+ - [unparser](https://github.com/mbj/unparser)
5
+ - [method_source](https://github.com/banister/method_source).
6
+
7
+ We usually describe policy rules using _boolean expressions_ (e.g. `A or (B and C)` where each of `A`, `B` and `C` is a simple boolean expression or predicate method).
8
+
9
+ When dealing with complex policies, it could be hard to figure out which predicate/check made policy to fail.
10
+
11
+ The `Policy#pp(rule)` method aims to help debug such situations.
12
+
13
+ Consider a (synthetic) example:
14
+
15
+ ```ruby
16
+ def feed?
17
+ (admin? || allowed_to?(:access_feed?)) &&
18
+ (user.name == "Jack" || user.name == "Kate")
19
+ end
20
+
21
+ ```
22
+
23
+ Suppose that you want to debug this rule ("Why does it return false?").
24
+ You can drop a [`binding.pry`](https://github.com/deivid-rodriguez/pry-byebug) (or `binding.irb`) right at the beginning of the method:
25
+
26
+ ```ruby
27
+ def feed?
28
+ binding.pry # rubocop:disable Lint/Debugger
29
+ #...
30
+ end
31
+ ```
32
+
33
+ Now, run your code and trigger the breakpoint (i.e., run the method):
34
+
35
+ ```
36
+ # now you can preview the execution of the rule using the `pp` method (defined on the policy instance)
37
+ pry> pp :feed?
38
+ MyPolicy#feed?
39
+ ↳ (
40
+ admin? #=> false
41
+ OR
42
+ allowed_to?(:access_feed?) #=> true
43
+ )
44
+ AND
45
+ (
46
+ user.name == "Jack" #=> false
47
+ OR
48
+ user.name == "Kate" #=> true
49
+ )
50
+
51
+ # you can also check other rules or methods as well
52
+ pry> pp :admin?
53
+ MyPolicy#admin?
54
+ ↳ user.admin? #=> false
55
+ ```
@@ -0,0 +1,27 @@
1
+ # Dealing with Decorators
2
+
3
+ Ref: [action_policy#7](https://github.com/palkan/action_policy/issues/7).
4
+
5
+ Since Action Policy [lookup mechanism](./lookup_chain.md) relies on the target
6
+ record's class properties (names, methods) it could break when using with _decorators_.
7
+
8
+ To make `authorize!` and other [behaviour](./behaviour.md) methods work seamlessly with decorated
9
+ objects, you might want to _enhance_ the `policy_for` method.
10
+
11
+ For example, when using the [Draper](https://github.com/drapergem/draper) gem:
12
+
13
+ ```ruby
14
+ module ActionPolicy
15
+ module Draper
16
+ def policy_for(record:, **opts)
17
+ # From https://github.com/GoodMeasuresLLC/draper-cancancan/blob/master/lib/draper/cancancan.rb
18
+ record = record.model while record.is_a?(Draper::Decorator)
19
+ super(record: record, **opts)
20
+ end
21
+ end
22
+ end
23
+
24
+ class ApplicationController < ActionController::Base
25
+ prepend ActionPolicy::Draper
26
+ end
27
+ ```