action_policy 0.2.4 → 0.3.0.beta1

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