action_policy 0.4.4 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +203 -174
  3. data/README.md +5 -4
  4. data/lib/action_policy.rb +7 -1
  5. data/lib/action_policy/behaviour.rb +22 -16
  6. data/lib/action_policy/behaviours/policy_for.rb +10 -3
  7. data/lib/action_policy/behaviours/scoping.rb +2 -1
  8. data/lib/action_policy/behaviours/thread_memoized.rb +1 -3
  9. data/lib/action_policy/ext/module_namespace.rb +1 -6
  10. data/lib/action_policy/ext/policy_cache_key.rb +10 -30
  11. data/lib/action_policy/i18n.rb +1 -1
  12. data/lib/action_policy/lookup_chain.rb +29 -15
  13. data/lib/action_policy/policy/aliases.rb +7 -12
  14. data/lib/action_policy/policy/authorization.rb +8 -7
  15. data/lib/action_policy/policy/cache.rb +11 -17
  16. data/lib/action_policy/policy/core.rb +25 -12
  17. data/lib/action_policy/policy/defaults.rb +3 -9
  18. data/lib/action_policy/policy/execution_result.rb +3 -9
  19. data/lib/action_policy/policy/pre_check.rb +19 -58
  20. data/lib/action_policy/policy/reasons.rb +29 -19
  21. data/lib/action_policy/policy/scoping.rb +5 -6
  22. data/lib/action_policy/rails/controller.rb +6 -1
  23. data/lib/action_policy/rails/policy/instrumentation.rb +1 -1
  24. data/lib/action_policy/rspec/be_authorized_to.rb +5 -9
  25. data/lib/action_policy/rspec/dsl.rb +1 -1
  26. data/lib/action_policy/rspec/have_authorized_scope.rb +5 -7
  27. data/lib/action_policy/utils/pretty_print.rb +21 -24
  28. data/lib/action_policy/utils/suggest_message.rb +1 -3
  29. data/lib/action_policy/version.rb +1 -1
  30. data/lib/generators/action_policy/install/templates/{application_policy.rb → application_policy.rb.tt} +0 -0
  31. data/lib/generators/action_policy/policy/policy_generator.rb +4 -1
  32. data/lib/generators/action_policy/policy/templates/{policy.rb → policy.rb.tt} +0 -0
  33. data/lib/generators/rspec/templates/{policy_spec.rb → policy_spec.rb.tt} +0 -0
  34. data/lib/generators/test_unit/templates/{policy_test.rb → policy_test.rb.tt} +0 -0
  35. metadata +29 -119
  36. data/.gitattributes +0 -2
  37. data/.github/ISSUE_TEMPLATE.md +0 -21
  38. data/.github/PULL_REQUEST_TEMPLATE.md +0 -29
  39. data/.github/bug_report_template.rb +0 -175
  40. data/.gitignore +0 -15
  41. data/.rubocop.yml +0 -54
  42. data/.tidelift.yml +0 -6
  43. data/.travis.yml +0 -31
  44. data/Gemfile +0 -22
  45. data/Rakefile +0 -27
  46. data/action_policy.gemspec +0 -44
  47. data/benchmarks/namespaced_lookup_cache.rb +0 -74
  48. data/benchmarks/pre_checks.rb +0 -73
  49. data/bin/console +0 -14
  50. data/bin/setup +0 -8
  51. data/docs/.nojekyll +0 -0
  52. data/docs/CNAME +0 -1
  53. data/docs/README.md +0 -79
  54. data/docs/_sidebar.md +0 -27
  55. data/docs/aliases.md +0 -122
  56. data/docs/assets/docsify-search.js +0 -364
  57. data/docs/assets/docsify.min.js +0 -3
  58. data/docs/assets/fonts/FiraCode-Medium.woff +0 -0
  59. data/docs/assets/fonts/FiraCode-Regular.woff +0 -0
  60. data/docs/assets/images/banner.png +0 -0
  61. data/docs/assets/images/cache.png +0 -0
  62. data/docs/assets/images/cache.svg +0 -70
  63. data/docs/assets/images/layer.png +0 -0
  64. data/docs/assets/images/layer.svg +0 -35
  65. data/docs/assets/prism-ruby.min.js +0 -1
  66. data/docs/assets/styles.css +0 -347
  67. data/docs/assets/vue.min.css +0 -1
  68. data/docs/authorization_context.md +0 -92
  69. data/docs/behaviour.md +0 -113
  70. data/docs/caching.md +0 -291
  71. data/docs/controller_action_aliases.md +0 -109
  72. data/docs/custom_lookup_chain.md +0 -48
  73. data/docs/custom_policy.md +0 -53
  74. data/docs/debugging.md +0 -55
  75. data/docs/decorators.md +0 -27
  76. data/docs/favicon.ico +0 -0
  77. data/docs/graphql.md +0 -302
  78. data/docs/i18n.md +0 -44
  79. data/docs/index.html +0 -43
  80. data/docs/instrumentation.md +0 -84
  81. data/docs/lookup_chain.md +0 -22
  82. data/docs/namespaces.md +0 -77
  83. data/docs/non_rails.md +0 -28
  84. data/docs/pre_checks.md +0 -57
  85. data/docs/pundit_migration.md +0 -80
  86. data/docs/quick_start.md +0 -118
  87. data/docs/rails.md +0 -120
  88. data/docs/reasons.md +0 -120
  89. data/docs/scoping.md +0 -255
  90. data/docs/testing.md +0 -390
  91. data/docs/writing_policies.md +0 -107
  92. data/gemfiles/jruby.gemfile +0 -8
  93. data/gemfiles/rails42.gemfile +0 -9
  94. data/gemfiles/rails6.gemfile +0 -8
  95. data/gemfiles/railsmaster.gemfile +0 -6
  96. data/lib/action_policy/ext/string_match.rb +0 -14
  97. 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,291 +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 (defined in `#rule_cache_key(rule)` method):
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` (or `#cache_key_with_version` for modern Rails versions). 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 `rule_cache_key` / `cache_namespace` / `context_cache_key` methods for policy class to override this logic.
183
-
184
- You can also use the `#cache` instance method to cache arbitrary values in you policies:
185
-
186
- ```ruby
187
- class ApplicationPolicy < ActionPolicy::Base
188
- # Suppose that a user has many roles each having an array of permissions
189
- def permissions
190
- cache(user) { user.roles.pluck(:permissions).flatten.uniq }
191
- end
192
-
193
- # You can pass multiple cache key "parts"
194
- def account_permissions(account)
195
- cache(user, account) { user.account_roles.where(account: account).pluck(:permissions).flatten.uniq }
196
- end
197
- end
198
- ```
199
-
200
- **NOTE:** `#cache` method uses the same cache key generation logic as rules caching (described above).
201
-
202
- #### Invalidation
203
-
204
- There no one-size-fits-all solution for invalidation. It highly depends on your business logic.
205
-
206
- **Case \#1**: no invalidation required.
207
-
208
- First of all, you should try to avoid manual invalidation at all. That could be achieved by using elaborate cache keys.
209
-
210
- Let's consider an example.
211
-
212
- 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`).
213
-
214
- Then you can do the following:
215
- - Keep tracking the last `Access` added/updated/deleted for resource (e.g. `Access.belongs_to :accessessable, touch: :access_updated_at`)
216
- - Use the following cache keys:
217
-
218
- ```ruby
219
- class User
220
- def policy_cache_key
221
- "user::#{id}::#{role_id}"
222
- end
223
- end
224
-
225
- class Resource
226
- def policy_cache_key
227
- "#{resource.class.name}::#{id}::#{access_updated_at}"
228
- end
229
- end
230
- ```
231
-
232
- **Case \#2**: discarding all cache at once.
233
-
234
- That's pretty easy: just override `cache_namespace` method in your `ApplicationPolicy` with the new value:
235
-
236
- ```ruby
237
- class ApplicationPolicy < ActionPolicy::Base
238
- # It's a good idea to store the changing part in the constant
239
- CACHE_VERSION = "v2".freeze
240
-
241
- # or even from the env variable
242
- # CACHE_VERSION = ENV.fetch("POLICY_CACHE_VERSION", "v2").freeze
243
-
244
- def cache_namespace
245
- "action_policy::#{CACHE_VERSION}"
246
- end
247
- end
248
- ```
249
-
250
- **Case \#3**: discarding some keys.
251
-
252
- That is an alternative approach to _crafting_ cache keys.
253
-
254
- If you have a limited number of places in your application where you update access control,
255
- you can invalidate policies cache manually. If your cache store supports `delete_matched` command (deleting keys using a wildcard), you can try the following:
256
-
257
- ```ruby
258
- class ApplicationPolicy < ActionPolicy::Base
259
- # Define custom cache key generator
260
- def cache_key(rule)
261
- "policy_cache/#{user.id}/#{self.class.name}/#{record.id}/#{rule}"
262
- end
263
- end
264
-
265
- class Access < ApplicationRecord
266
- belongs_to :resource
267
- belongs_to :user
268
-
269
- after_commit :cleanup_policy_cache, on: [:create, :destroy]
270
-
271
- def cleanup_policy_cache
272
- # Clear cache for the corresponding user-record pair
273
- ActionPolicy.cache_store.delete_matched(
274
- "policy_cache/#{user_id}/#{ResourcePolicy.name}/#{resource_id}/*"
275
- )
276
- end
277
- end
278
-
279
- class User < ApplicationRecord
280
- belongs_to :role
281
-
282
- after_commit :cleanup_policy_cache, on: [:update], if: :role_id_changed?
283
-
284
- def cleanup_policy_cache
285
- # Clear all policies cache for user
286
- ActionPolicy.cache_store.delete_matched(
287
- "policy_cache/#{user_id}/*"
288
- )
289
- end
290
- end
291
- ```