action_policy 0.4.0 → 0.5.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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +233 -171
  3. data/LICENSE.txt +1 -1
  4. data/README.md +7 -11
  5. data/lib/action_policy.rb +7 -1
  6. data/lib/action_policy/behaviour.rb +22 -16
  7. data/lib/action_policy/behaviours/policy_for.rb +10 -3
  8. data/lib/action_policy/behaviours/scoping.rb +2 -1
  9. data/lib/action_policy/behaviours/thread_memoized.rb +1 -3
  10. data/lib/action_policy/ext/module_namespace.rb +1 -6
  11. data/lib/action_policy/ext/policy_cache_key.rb +15 -33
  12. data/lib/action_policy/ext/{symbol_classify.rb → symbol_camelize.rb} +6 -6
  13. data/lib/action_policy/i18n.rb +1 -1
  14. data/lib/action_policy/lookup_chain.rb +41 -21
  15. data/lib/action_policy/policy/aliases.rb +7 -12
  16. data/lib/action_policy/policy/authorization.rb +14 -17
  17. data/lib/action_policy/policy/cache.rb +34 -18
  18. data/lib/action_policy/policy/core.rb +25 -12
  19. data/lib/action_policy/policy/defaults.rb +3 -9
  20. data/lib/action_policy/policy/execution_result.rb +3 -9
  21. data/lib/action_policy/policy/pre_check.rb +19 -58
  22. data/lib/action_policy/policy/reasons.rb +30 -20
  23. data/lib/action_policy/policy/scoping.rb +5 -6
  24. data/lib/action_policy/rails/controller.rb +6 -1
  25. data/lib/action_policy/rails/ext/active_record.rb +7 -0
  26. data/lib/action_policy/rails/policy/instrumentation.rb +1 -1
  27. data/lib/action_policy/rspec/be_authorized_to.rb +5 -9
  28. data/lib/action_policy/rspec/dsl.rb +3 -3
  29. data/lib/action_policy/rspec/have_authorized_scope.rb +5 -7
  30. data/lib/action_policy/testing.rb +1 -1
  31. data/lib/action_policy/utils/pretty_print.rb +21 -24
  32. data/lib/action_policy/utils/suggest_message.rb +1 -3
  33. data/lib/action_policy/version.rb +1 -1
  34. data/lib/generators/action_policy/install/templates/{application_policy.rb → application_policy.rb.tt} +1 -1
  35. data/lib/generators/action_policy/policy/policy_generator.rb +4 -1
  36. data/lib/generators/action_policy/policy/templates/{policy.rb → policy.rb.tt} +0 -0
  37. data/lib/generators/rspec/templates/{policy_spec.rb → policy_spec.rb.tt} +0 -0
  38. data/lib/generators/test_unit/templates/{policy_test.rb → policy_test.rb.tt} +0 -0
  39. metadata +30 -119
  40. data/.gitattributes +0 -2
  41. data/.github/FUNDING.yml +0 -1
  42. data/.github/ISSUE_TEMPLATE.md +0 -18
  43. data/.github/PULL_REQUEST_TEMPLATE.md +0 -29
  44. data/.gitignore +0 -15
  45. data/.rubocop.yml +0 -54
  46. data/.tidelift.yml +0 -6
  47. data/.travis.yml +0 -31
  48. data/Gemfile +0 -22
  49. data/Rakefile +0 -27
  50. data/action_policy.gemspec +0 -44
  51. data/benchmarks/namespaced_lookup_cache.rb +0 -71
  52. data/bin/console +0 -14
  53. data/bin/setup +0 -8
  54. data/docs/.nojekyll +0 -0
  55. data/docs/CNAME +0 -1
  56. data/docs/README.md +0 -77
  57. data/docs/_sidebar.md +0 -27
  58. data/docs/aliases.md +0 -122
  59. data/docs/assets/docsify-search.js +0 -364
  60. data/docs/assets/docsify.min.js +0 -3
  61. data/docs/assets/fonts/FiraCode-Medium.woff +0 -0
  62. data/docs/assets/fonts/FiraCode-Regular.woff +0 -0
  63. data/docs/assets/images/banner.png +0 -0
  64. data/docs/assets/images/cache.png +0 -0
  65. data/docs/assets/images/cache.svg +0 -70
  66. data/docs/assets/images/layer.png +0 -0
  67. data/docs/assets/images/layer.svg +0 -35
  68. data/docs/assets/prism-ruby.min.js +0 -1
  69. data/docs/assets/styles.css +0 -347
  70. data/docs/assets/vue.min.css +0 -1
  71. data/docs/authorization_context.md +0 -92
  72. data/docs/behaviour.md +0 -113
  73. data/docs/caching.md +0 -273
  74. data/docs/controller_action_aliases.md +0 -109
  75. data/docs/custom_lookup_chain.md +0 -48
  76. data/docs/custom_policy.md +0 -53
  77. data/docs/debugging.md +0 -55
  78. data/docs/decorators.md +0 -27
  79. data/docs/favicon.ico +0 -0
  80. data/docs/graphql.md +0 -302
  81. data/docs/i18n.md +0 -44
  82. data/docs/index.html +0 -43
  83. data/docs/instrumentation.md +0 -84
  84. data/docs/lookup_chain.md +0 -17
  85. data/docs/namespaces.md +0 -77
  86. data/docs/non_rails.md +0 -28
  87. data/docs/pre_checks.md +0 -57
  88. data/docs/pundit_migration.md +0 -80
  89. data/docs/quick_start.md +0 -118
  90. data/docs/rails.md +0 -120
  91. data/docs/reasons.md +0 -120
  92. data/docs/scoping.md +0 -255
  93. data/docs/testing.md +0 -333
  94. data/docs/writing_policies.md +0 -107
  95. data/gemfiles/jruby.gemfile +0 -8
  96. data/gemfiles/rails42.gemfile +0 -8
  97. data/gemfiles/rails6.gemfile +0 -8
  98. data/gemfiles/railsmaster.gemfile +0 -6
  99. data/lib/action_policy/ext/string_match.rb +0 -14
  100. data/lib/action_policy/ext/yield_self_then.rb +0 -25
@@ -1 +0,0 @@
1
- @import url("https://fonts.googleapis.com/css?family=Roboto+Mono|Source+Sans+Pro:300,400,600");*{-webkit-font-smoothing:antialiased;-webkit-overflow-scrolling:touch;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-text-size-adjust:none;-webkit-touch-callout:none;box-sizing:border-box}body:not(.ready){overflow:hidden}body:not(.ready) .app-nav,body:not(.ready)>nav,body:not(.ready) [data-cloak]{display:none}div#app{font-size:30px;font-weight:lighter;margin:40vh auto;text-align:center}div#app:empty:before{content:"Loading..."}.emoji{height:1.2rem;vertical-align:middle}.progress{background-color:var(--theme-color,#42b983);height:2px;left:0;position:fixed;right:0;top:0;transition:width .2s,opacity .4s;width:0;z-index:5}.search .search-keyword,.search a:hover{color:var(--theme-color,#42b983)}.search .search-keyword{font-style:normal;font-weight:700}body,html{height:100%}body{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;color:#34495e;font-family:Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:15px;letter-spacing:0;margin:0;overflow-x:hidden}img{max-width:100%}a[disabled]{cursor:not-allowed;opacity:.6}kbd{border:1px solid #ccc;border-radius:3px;display:inline-block;font-size:12px!important;line-height:12px;margin-bottom:3px;padding:3px 5px;vertical-align:middle}.task-list-item{list-style-type:none}li input[type=checkbox]{margin:0 .2em .25em -1.6em;vertical-align:middle}.app-nav{margin:25px 60px 0 0;position:absolute;right:0;text-align:right;z-index:2}.app-nav.no-badge{margin-right:25px}.app-nav p{margin:0}.app-nav>a{margin:0 1rem;padding:5px 0}.app-nav li,.app-nav ul{display:inline-block;list-style:none;margin:0}.app-nav a{color:inherit;font-size:16px;text-decoration:none;transition:color .3s}.app-nav a.active,.app-nav a:hover{color:var(--theme-color,#42b983)}.app-nav a.active{border-bottom:2px solid var(--theme-color,#42b983)}.app-nav li{display:inline-block;margin:0 1rem;padding:5px 0;position:relative}.app-nav li ul{background-color:#fff;border:1px solid #ddd;border-bottom-color:#ccc;border-radius:4px;box-sizing:border-box;display:none;max-height:calc(100vh - 61px);overflow-y:auto;padding:10px 0;position:absolute;right:-15px;text-align:left;top:100%;white-space:nowrap}.app-nav li ul li{display:block;font-size:14px;line-height:1rem;margin:0;margin:8px 14px;white-space:nowrap}.app-nav li ul a{display:block;font-size:inherit;margin:0;padding:0}.app-nav li ul a.active{border-bottom:0}.app-nav li:hover ul{display:block}.github-corner{border-bottom:0;position:fixed;right:0;text-decoration:none;top:0;z-index:1}.github-corner:hover .octo-arm{animation:a .56s ease-in-out}.github-corner svg{color:#fff;fill:var(--theme-color,#42b983);height:80px;width:80px}main{display:block;position:relative;width:100vw;height:100%;z-index:0}main.hidden{display:none}.anchor{display:inline-block;text-decoration:none;transition:all .3s}.anchor span{color:#34495e}.anchor:hover{text-decoration:underline}.sidebar{border-right:1px solid rgba(0,0,0,.07);overflow-y:auto;padding:40px 0 0;position:absolute;top:0;bottom:0;left:0;transition:transform .25s ease-out;width:300px;z-index:3}.sidebar>h1{margin:0 auto 1rem;font-size:1.5rem;font-weight:300;text-align:center}.sidebar>h1 a{color:inherit;text-decoration:none}.sidebar>h1 .app-nav{display:block;position:static}.sidebar .sidebar-nav{line-height:2em;padding-bottom:40px}.sidebar li.collapse .app-sub-sidebar{display:none}.sidebar ul{margin:0;padding:0}.sidebar li>p{font-weight:700;margin:0}.sidebar ul,.sidebar ul li{list-style:none}.sidebar ul li a{border-bottom:none;display:block}.sidebar ul li ul{padding-left:20px}.sidebar::-webkit-scrollbar{width:4px}.sidebar::-webkit-scrollbar-thumb{background:transparent;border-radius:4px}.sidebar:hover::-webkit-scrollbar-thumb{background:hsla(0,0%,53%,.4)}.sidebar:hover::-webkit-scrollbar-track{background:hsla(0,0%,53%,.1)}.sidebar-toggle{background-color:transparent;background-color:hsla(0,0%,100%,.8);border:0;outline:none;padding:10px;position:absolute;bottom:0;left:0;text-align:center;transition:opacity .3s;width:284px;z-index:4}.sidebar-toggle .sidebar-toggle-button:hover{opacity:.4}.sidebar-toggle span{background-color:var(--theme-color,#42b983);display:block;margin-bottom:4px;width:16px;height:2px}body.sticky .sidebar,body.sticky .sidebar-toggle{position:fixed}.content{padding-top:60px;position:absolute;top:0;right:0;bottom:0;left:300px;transition:left .25s ease}.markdown-section{margin:0 auto;max-width:800px;padding:30px 15px 40px;position:relative}.markdown-section>*{box-sizing:border-box;font-size:inherit}.markdown-section>:first-child{margin-top:0!important}.markdown-section hr{border:none;border-bottom:1px solid #eee;margin:2em 0}.markdown-section iframe{border:1px solid #eee}.markdown-section table{border-collapse:collapse;border-spacing:0;display:block;margin-bottom:1rem;overflow:auto;width:100%}.markdown-section th{font-weight:700}.markdown-section td,.markdown-section th{border:1px solid #ddd;padding:6px 13px}.markdown-section tr{border-top:1px solid #ccc}.markdown-section p.tip,.markdown-section tr:nth-child(2n){background-color:#f8f8f8}.markdown-section p.tip{border-bottom-right-radius:2px;border-left:4px solid #f66;border-top-right-radius:2px;margin:2em 0;padding:12px 24px 12px 30px;position:relative}.markdown-section p.tip:before{background-color:#f66;border-radius:100%;color:#fff;content:"!";font-family:Dosis,Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:14px;font-weight:700;left:-12px;line-height:20px;position:absolute;height:20px;width:20px;text-align:center;top:14px}.markdown-section p.tip code{background-color:#efefef}.markdown-section p.tip em{color:#34495e}.markdown-section p.warn{background:rgba(66,185,131,.1);border-radius:2px;padding:1rem}body.close .sidebar{transform:translateX(-300px)}body.close .sidebar-toggle{width:auto}body.close .content{left:0}@media print{.app-nav,.github-corner,.sidebar,.sidebar-toggle{display:none}}@media screen and (max-width:768px){.github-corner,.sidebar,.sidebar-toggle{position:fixed}.app-nav{margin-top:16px}.app-nav li ul{top:30px}main{height:auto;overflow-x:hidden}.sidebar{left:-300px;transition:transform .25s ease-out}.content{left:0;max-width:100vw;position:static;padding-top:20px;transition:transform .25s ease}.app-nav,.github-corner{transition:transform .25s ease-out}.sidebar-toggle{background-color:transparent;width:auto;padding:30px 30px 10px 10px}body.close .sidebar{transform:translateX(300px)}body.close .sidebar-toggle{background-color:hsla(0,0%,100%,.8);transition:background-color 1s;width:284px;padding:10px}body.close .content{transform:translateX(300px)}body.close .app-nav,body.close .github-corner{display:none}.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:a .56s ease-in-out}}@keyframes a{0%,to{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}section.cover{-ms-flex-align:center;align-items:center;background-position:50%;background-repeat:no-repeat;background-size:cover;height:100vh;display:none}section.cover.show{display:-ms-flexbox;display:flex}section.cover.has-mask .mask{background-color:#fff;opacity:.8;position:absolute;top:0;height:100%;width:100%}section.cover .cover-main{-ms-flex:1;flex:1;margin:-20px 16px 0;text-align:center;z-index:1}section.cover a{color:inherit}section.cover a,section.cover a:hover{text-decoration:none}section.cover p{line-height:1.5rem;margin:1em 0}section.cover h1{color:inherit;font-size:2.5rem;font-weight:300;margin:.625rem 0 2.5rem;position:relative;text-align:center}section.cover h1 a{display:block}section.cover h1 small{bottom:-.4375rem;font-size:1rem;position:absolute}section.cover blockquote{font-size:1.5rem;text-align:center}section.cover ul{line-height:1.8;list-style-type:none;margin:1em auto;max-width:500px;padding:0}section.cover .cover-main>p:last-child a{border:1px solid var(--theme-color,#42b983);border-radius:2rem;box-sizing:border-box;color:var(--theme-color,#42b983);display:inline-block;font-size:1.05rem;letter-spacing:.1rem;margin:.5rem 1rem;padding:.75em 2rem;text-decoration:none;transition:all .15s ease}section.cover .cover-main>p:last-child a:last-child{background-color:var(--theme-color,#42b983);color:#fff}section.cover .cover-main>p:last-child a:last-child:hover{color:inherit;opacity:.8}section.cover .cover-main>p:last-child a:hover{color:inherit}section.cover blockquote>p>a{border-bottom:2px solid var(--theme-color,#42b983);transition:color .3s}section.cover blockquote>p>a:hover{color:var(--theme-color,#42b983)}.sidebar,body{background-color:#fff}.sidebar{color:#364149}.sidebar li{margin:6px 0 6px 15px}.sidebar ul li a{color:#505d6b;font-size:14px;font-weight:400;overflow:hidden;text-decoration:none;text-overflow:ellipsis;white-space:nowrap}.sidebar ul li a:hover{text-decoration:underline}.sidebar ul li ul{padding:0}.sidebar ul li.active>a{border-right:2px solid;color:var(--theme-color,#42b983);font-weight:600}.app-sub-sidebar li:before{content:"-";padding-right:4px;float:left}.markdown-section h1,.markdown-section h2,.markdown-section h3,.markdown-section h4,.markdown-section strong{color:#2c3e50;font-weight:600}.markdown-section a{color:var(--theme-color,#42b983);font-weight:600}.markdown-section h1{font-size:2rem;margin:0 0 1rem}.markdown-section h2{font-size:1.75rem;margin:45px 0 .8rem}.markdown-section h3{font-size:1.5rem;margin:40px 0 .6rem}.markdown-section h4{font-size:1.25rem}.markdown-section h5{font-size:1rem}.markdown-section h6{color:#777;font-size:1rem}.markdown-section figure,.markdown-section p{margin:1.2em 0}.markdown-section ol,.markdown-section p,.markdown-section ul{line-height:1.6rem;word-spacing:.05rem}.markdown-section ol,.markdown-section ul{padding-left:1.5rem}.markdown-section blockquote{border-left:4px solid var(--theme-color,#42b983);color:#858585;margin:2em 0;padding-left:20px}.markdown-section blockquote p{font-weight:600;margin-left:0}.markdown-section iframe{margin:1em 0}.markdown-section em{color:#7f8c8d}.markdown-section code{border-radius:2px;color:#e96900;font-size:.8rem;margin:0 2px;padding:3px 5px;white-space:pre-wrap}.markdown-section code,.markdown-section pre{background-color:#f8f8f8;font-family:Roboto Mono,Monaco,courier,monospace}.markdown-section pre{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;line-height:1.5rem;margin:1.2em 0;overflow:auto;padding:0 1.4rem;position:relative;word-wrap:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8e908c}.token.namespace{opacity:.7}.token.boolean,.token.number{color:#c76b29}.token.punctuation{color:#525252}.token.property{color:#c08b30}.token.tag{color:#2973b7}.token.string{color:var(--theme-color,#42b983)}.token.selector{color:#6679cc}.token.attr-name{color:#2973b7}.language-css .token.string,.style .token.string,.token.entity,.token.url{color:#22a2c9}.token.attr-value,.token.control,.token.directive,.token.unit{color:var(--theme-color,#42b983)}.token.keyword{color:#e96900}.token.atrule,.token.regex,.token.statement{color:#22a2c9}.token.placeholder,.token.variable{color:#3d8fd1}.token.deleted{text-decoration:line-through}.token.inserted{border-bottom:1px dotted #202746;text-decoration:none}.token.italic{font-style:italic}.token.bold,.token.important{font-weight:700}.token.important{color:#c94922}.token.entity{cursor:help}.markdown-section pre>code{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;background-color:#f8f8f8;border-radius:2px;color:#525252;display:block;font-family:Roboto Mono,Monaco,courier,monospace;font-size:.8rem;line-height:inherit;margin:0 2px;max-width:inherit;overflow:inherit;padding:2.2em 5px;white-space:inherit}.markdown-section code:after,.markdown-section code:before{letter-spacing:.05rem}code .token{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;min-height:1.5rem}pre:after{color:#ccc;content:attr(data-lang);font-size:.6rem;font-weight:600;height:15px;line-height:15px;padding:5px 10px 0;position:absolute;right:0;text-align:right;top:0}
@@ -1,92 +0,0 @@
1
- # Authorization Context
2
-
3
- _Authorization context_ contains information about the acting subject.
4
-
5
- In most cases, it is just a _user_, but sometimes it could be a composition of subjects.
6
-
7
- You must configure authorization context in **two places**: in the policy itself and in the place where you perform the authorization (e.g., controllers).
8
-
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
-
11
- To specify additional contexts, you should use the `authorize` method:
12
-
13
- ```ruby
14
- class ApplicationPolicy < ActionPolicy::Base
15
- authorize :account
16
- end
17
- ```
18
-
19
- Now you must provide `account` during policy initialization. When authorization key is missing or equals to `nil`, `ActionPolicy::AuthorizationContextMissing` error is raised.
20
-
21
- **NOTE:** if you want to allow passing `nil` as `account` value, you must add `allow_nil: true` option to `authorize`.
22
- If you want to be able not to pass `account` at all, you must add `optional: true`
23
-
24
- To do that automatically in your `authorize!` and `allowed_to?` calls, you must also configure authorization context. For example, in your controller:
25
-
26
- ```ruby
27
- class ApplicationController < ActionController::Base
28
- # First argument should be the same as in the policy.
29
- # `through` specifies the method name to be called to
30
- # get the required context object
31
- # (equals to the context name itself by default, i.e. `account`)
32
- authorize :account, through: :current_account
33
- end
34
- ```
35
-
36
- ## Nested Policies vs Contexts
37
-
38
- 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)
39
-
40
- When you call another policy from the policy object (e.g. via `allowed_to?` method),
41
- the context of the current policy is passed to the _nested_ policy.
42
-
43
- That means that if the nested policy has a different authorization context, we won't be able
44
- to build it (event if you configure all the required keys in the controller).
45
-
46
- For example:
47
-
48
- ```ruby
49
- class UserPolicy < ActionPolicy::Base
50
- authorize :user
51
-
52
- def show?
53
- allowed_to?(:show?, record.profile)
54
- end
55
- end
56
-
57
- class ProfilePolicy < ActionPolicy::Base
58
- authorize :user, :account
59
- end
60
-
61
- class ApplicationController < ActionController::Base
62
- authorize :user, through: :current_user
63
- authorize :account, through: :current_account
64
- end
65
-
66
- class UsersController < ApplicationController
67
- def show
68
- user = User.find(params[:id])
69
-
70
- authorize! user #=> raises "Missing policy authorization context: account"
71
- end
72
- end
73
- ```
74
-
75
- 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).
76
-
77
-
78
- ## Explicit context
79
-
80
- You can override the _implicit_ authorization context (generated with `authorize` method) in-place
81
- by passing the `context` option:
82
-
83
- ```ruby
84
- def show
85
- user = User.find(params[:id])
86
-
87
- authorize! user, context: {account: user.account}
88
- end
89
- ```
90
-
91
- **NOTE:** the explictly provided context is merged with the implicit one (i.e. you can specify
92
- only the keys you want to override).
@@ -1,113 +0,0 @@
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
- ```
@@ -1,273 +0,0 @@
1
- # Caching
2
-
3
- Action Policy aims to be as performant as possible. One of the ways to accomplish that is to include a comprehensive caching system.
4
-
5
- There are several cache layers available: rule-level memoization, local (instance-level) memoization, and _external_ cache (through cache stores).
6
-
7
- <div class="chart-container">
8
- <img src="assets/images/cache.svg" alt="Cache layers" width="60%">
9
- </div>
10
-
11
- ## Policy memoization
12
-
13
- ### Per-instance
14
-
15
- There could be a situation when you need to apply the same policy to the same record multiple times during the action (e.g., request). For example:
16
-
17
- ```ruby
18
- # app/controllers/posts_controller.rb
19
- class PostsController < ApplicationController
20
- def show
21
- @post = Post.find(params[:id])
22
- authorize! @post
23
- render :show
24
- end
25
- end
26
- ```
27
-
28
- ```erb
29
- # app/views/posts/show.html.erb
30
- <h1><%= @post.title %>
31
-
32
- <% if allowed_to?(:edit?, @post) %>
33
- <%= link_to "Edit", @post %>
34
- <% end %>
35
-
36
- <% if allowed_to?(:destroy?, @post) %>
37
- <%= link_to "Delete", @post, method: :delete %>
38
- <% end %>
39
- ```
40
-
41
- In the above example, we need to use the same policy three times. Action Policy re-uses the policy instance to avoid unnecessary object allocation.
42
-
43
- We rely on the following assumptions:
44
- - parent object (e.g., a controller instance) is _ephemeral_, i.e., it is a short-lived object
45
- - all authorizations use the same [authorization context](authorization_context.md).
46
-
47
- We use `record.policy_cache_key` with fallback to `record.cache_key` or `record.object_id` as a part of policy identifier in the local store.
48
-
49
- **NOTE**: policies memoization is an extension for `ActionPolicy::Behaviour` and could be included with `ActionPolicy::Behaviours::Memoized`.
50
-
51
- **NOTE**: memoization is automatically included into Rails controllers integration, but not included into channels integration, since channels are long-lived objects.
52
-
53
- ### Per-thread
54
-
55
- Consider a more complex situation:
56
-
57
- ```ruby
58
- # app/controllers/comments_controller.rb
59
- class CommentsController < ApplicationController
60
- def index
61
- # all comments for all posts
62
- @comments = Comment.all
63
- end
64
- end
65
- ```
66
-
67
- ```erb
68
- # app/views/comments/index.html.erb
69
- <% @comments.each do |comment| %>
70
- <li><%= comment.text %>
71
- <% if allowed_to?(:edit?, comment) %>
72
- <%= link_to comment, "Edit" %>
73
- <% end %>
74
- </li>
75
- <% end %>
76
- ```
77
-
78
- ```ruby
79
- # app/policies/comment_policy.rb
80
- class CommentPolicy < ApplicationPolicy
81
- def edit?
82
- user.admin? || (user.id == record.id) ||
83
- allowed_to?(:manage?, record.post)
84
- end
85
- end
86
- ```
87
-
88
- In some cases, we have to initialize **two** policies for each comment: one for the comment itself and one for the comment's post (in the `allowed_to?` call).
89
-
90
- That is an example of a _N+1 authorization_ problem, which in its turn could easily cause a _N+1 query_ problem (if `PostPolicy#manage?` makes database queries). Sounds terrible, doesn't it?
91
-
92
- It is likely that many comments belong to the same post. If so, we can move our memoization one level up and use local thread store.
93
-
94
- Action Policy provides `ActionPolicy::Behaviours::ThreadMemoized` module with this functionality (included into Rails controllers integration by default).
95
-
96
- If you want to add this behavior to your custom authorization-aware class, you should care about cleaning up the thread store manually (by calling `ActionPolicy::PerThreadCache.clear_all`).
97
-
98
- **NOTE:** per-thread cache is disabled by default in test environment (when either `RACK_ENV` or `RAILS_ENV` environment variable is equal to "test").
99
- You can turn it on (or off) by setting:
100
-
101
- ```ruby
102
- ActionPolicy::PerThreadCache.enabled = true # or false to disable
103
- ```
104
-
105
- ## Rule cache
106
-
107
- ### Per-instance
108
-
109
- There could be a situation when the same rule is called multiple times for the same policy instance (for example, when using [aliases](aliases.md)).
110
-
111
- In that case, Action Policy invokes the rule method only once, remembers the result, and returns it immediately for the subsequent calls.
112
-
113
- **NOTE**: rule results memoization is available only if you inherit from `ActionPolicy::Base` or include `ActionPolicy::Policy::CachedApply` into your `ApplicationPolicy`.
114
-
115
- ### Using the cache store
116
-
117
- Some policy rules might be _performance-heavy_, e.g., make complex database queries.
118
-
119
- In that case, it makes sense to cache the rule application result for a long time (not just for the duration of a request).
120
-
121
- Action Policy provides a way to use _cache stores_ for that. You have to explicitly define which rules you want to cache in your policy class. For example:
122
-
123
- ```ruby
124
- class StagePolicy < ApplicationPolicy
125
- # mark show? rule to be cached
126
- cache :show?
127
- # you can also provide store-specific options
128
- # cache :show?, expires_in: 1.hour
129
-
130
- def show?
131
- full_access? ||
132
- user.stage_permissions.where(
133
- stage_id: record.id
134
- ).exists?
135
- end
136
-
137
- private
138
-
139
- def full_access?
140
- !record.funnel.is_private? ||
141
- user.permissions
142
- .where(
143
- funnel_id: record.funnel_id,
144
- full_access: true
145
- ).exists?
146
- end
147
- end
148
- ```
149
-
150
- You must configure a cache store to use this feature:
151
-
152
- ```ruby
153
- ActionPolicy.cache_store = MyCacheStore.new
154
- ```
155
-
156
- Or, in Rails:
157
-
158
- ```ruby
159
- # config/application.rb (or config/environments/<environment>.rb)
160
- Rails.application.configure do |config|
161
- config.action_policy.cache_store = :redis_cache_store
162
- end
163
- ```
164
-
165
- Cache store must provide at least a `#read(key)` and `write(key, value, **options)` methods.
166
-
167
- **NOTE:** cache store also should take care of serialiation/deserialization since the `value` is `ExecutionResult` instance (which contains also some additional information, e.g. failure reasons). Rails cache store supports serialization/deserialization out-of-the-box.
168
-
169
- By default, Action Policy builds a cache key using the following scheme:
170
-
171
- ```ruby
172
- "#{cache_namespace}/#{context_cache_key}" \
173
- "/#{record.policy_cache_key}/#{policy.class.name}/#{rule}"
174
- ```
175
-
176
- Where `cache_namespace` is equal to `"acp:#{MAJOR_GEM_VERSION}.#{MINOR_GEM_VERSION}"`, and `context_cache_key` is a concatenation of all authorization contexts cache keys (in the same order as they are defined in the policy class).
177
-
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
-
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
-
182
- You can define your own `cache_key` / `cache_namespace` / `context_cache_key` methods for policy class to override this logic.
183
-
184
- #### Invalidation
185
-
186
- There no one-size-fits-all solution for invalidation. It highly depends on your business logic.
187
-
188
- **Case \#1**: no invalidation required.
189
-
190
- First of all, you should try to avoid manual invalidation at all. That could be achieved by using elaborate cache keys.
191
-
192
- Let's consider an example.
193
-
194
- Suppose that your users have _roles_ (i.e. `User.belongs_to :role`) and you give access to resources through the `Access` model (i.e. `Resource.has_many :accesses`).
195
-
196
- Then you can do the following:
197
- - Keep tracking the last `Access` added/updated/deleted for resource (e.g. `Access.belongs_to :accessessable, touch: :access_updated_at`)
198
- - Use the following cache keys:
199
-
200
- ```ruby
201
- class User
202
- def policy_cache_key
203
- "user::#{id}::#{role_id}"
204
- end
205
- end
206
-
207
- class Resource
208
- def policy_cache_key
209
- "#{resource.class.name}::#{id}::#{access_updated_at}"
210
- end
211
- end
212
- ```
213
-
214
- **Case \#2**: discarding all cache at once.
215
-
216
- That's pretty easy: just override `cache_namespace` method in your `ApplicationPolicy` with the new value:
217
-
218
- ```ruby
219
- class ApplicationPolicy < ActionPolicy::Base
220
- # It's a good idea to store the changing part in the constant
221
- CACHE_VERSION = "v2".freeze
222
-
223
- # or even from the env variable
224
- # CACHE_VERSION = ENV.fetch("POLICY_CACHE_VERSION", "v2").freeze
225
-
226
- def cache_namespace
227
- "action_policy::#{CACHE_VERSION}"
228
- end
229
- end
230
- ```
231
-
232
- **Case \#3**: discarding some keys.
233
-
234
- That is an alternative approach to _crafting_ cache keys.
235
-
236
- If you have a limited number of places in your application where you update access control,
237
- you can invalidate policies cache manually. If your cache store supports `delete_matched` command (deleting keys using a wildcard), you can try the following:
238
-
239
- ```ruby
240
- class ApplicationPolicy < ActionPolicy::Base
241
- # Define custom cache key generator
242
- def cache_key(rule)
243
- "policy_cache/#{user.id}/#{self.class.name}/#{record.id}/#{rule}"
244
- end
245
- end
246
-
247
- class Access < ApplicationRecord
248
- belongs_to :resource
249
- belongs_to :user
250
-
251
- after_commit :cleanup_policy_cache, on: [:create, :destroy]
252
-
253
- def cleanup_policy_cache
254
- # Clear cache for the corresponding user-record pair
255
- ActionPolicy.cache_store.delete_matched(
256
- "policy_cache/#{user_id}/#{ResourcePolicy.name}/#{resource_id}/*"
257
- )
258
- end
259
- end
260
-
261
- class User < ApplicationRecord
262
- belongs_to :role
263
-
264
- after_commit :cleanup_policy_cache, on: [:update], if: :role_id_changed?
265
-
266
- def cleanup_policy_cache
267
- # Clear all policies cache for user
268
- ActionPolicy.cache_store.delete_matched(
269
- "policy_cache/#{user_id}/*"
270
- )
271
- end
272
- end
273
- ```