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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +233 -171
- data/LICENSE.txt +1 -1
- data/README.md +7 -11
- data/lib/action_policy.rb +7 -1
- data/lib/action_policy/behaviour.rb +22 -16
- data/lib/action_policy/behaviours/policy_for.rb +10 -3
- data/lib/action_policy/behaviours/scoping.rb +2 -1
- data/lib/action_policy/behaviours/thread_memoized.rb +1 -3
- data/lib/action_policy/ext/module_namespace.rb +1 -6
- data/lib/action_policy/ext/policy_cache_key.rb +15 -33
- data/lib/action_policy/ext/{symbol_classify.rb → symbol_camelize.rb} +6 -6
- data/lib/action_policy/i18n.rb +1 -1
- data/lib/action_policy/lookup_chain.rb +41 -21
- data/lib/action_policy/policy/aliases.rb +7 -12
- data/lib/action_policy/policy/authorization.rb +14 -17
- data/lib/action_policy/policy/cache.rb +34 -18
- data/lib/action_policy/policy/core.rb +25 -12
- data/lib/action_policy/policy/defaults.rb +3 -9
- data/lib/action_policy/policy/execution_result.rb +3 -9
- data/lib/action_policy/policy/pre_check.rb +19 -58
- data/lib/action_policy/policy/reasons.rb +30 -20
- data/lib/action_policy/policy/scoping.rb +5 -6
- data/lib/action_policy/rails/controller.rb +6 -1
- data/lib/action_policy/rails/ext/active_record.rb +7 -0
- data/lib/action_policy/rails/policy/instrumentation.rb +1 -1
- data/lib/action_policy/rspec/be_authorized_to.rb +5 -9
- data/lib/action_policy/rspec/dsl.rb +3 -3
- data/lib/action_policy/rspec/have_authorized_scope.rb +5 -7
- data/lib/action_policy/testing.rb +1 -1
- data/lib/action_policy/utils/pretty_print.rb +21 -24
- data/lib/action_policy/utils/suggest_message.rb +1 -3
- data/lib/action_policy/version.rb +1 -1
- data/lib/generators/action_policy/install/templates/{application_policy.rb → application_policy.rb.tt} +1 -1
- data/lib/generators/action_policy/policy/policy_generator.rb +4 -1
- data/lib/generators/action_policy/policy/templates/{policy.rb → policy.rb.tt} +0 -0
- data/lib/generators/rspec/templates/{policy_spec.rb → policy_spec.rb.tt} +0 -0
- data/lib/generators/test_unit/templates/{policy_test.rb → policy_test.rb.tt} +0 -0
- metadata +30 -119
- data/.gitattributes +0 -2
- data/.github/FUNDING.yml +0 -1
- data/.github/ISSUE_TEMPLATE.md +0 -18
- data/.github/PULL_REQUEST_TEMPLATE.md +0 -29
- data/.gitignore +0 -15
- data/.rubocop.yml +0 -54
- data/.tidelift.yml +0 -6
- data/.travis.yml +0 -31
- data/Gemfile +0 -22
- data/Rakefile +0 -27
- data/action_policy.gemspec +0 -44
- data/benchmarks/namespaced_lookup_cache.rb +0 -71
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/docs/.nojekyll +0 -0
- data/docs/CNAME +0 -1
- data/docs/README.md +0 -77
- data/docs/_sidebar.md +0 -27
- data/docs/aliases.md +0 -122
- data/docs/assets/docsify-search.js +0 -364
- data/docs/assets/docsify.min.js +0 -3
- data/docs/assets/fonts/FiraCode-Medium.woff +0 -0
- data/docs/assets/fonts/FiraCode-Regular.woff +0 -0
- data/docs/assets/images/banner.png +0 -0
- data/docs/assets/images/cache.png +0 -0
- data/docs/assets/images/cache.svg +0 -70
- data/docs/assets/images/layer.png +0 -0
- data/docs/assets/images/layer.svg +0 -35
- data/docs/assets/prism-ruby.min.js +0 -1
- data/docs/assets/styles.css +0 -347
- data/docs/assets/vue.min.css +0 -1
- data/docs/authorization_context.md +0 -92
- data/docs/behaviour.md +0 -113
- data/docs/caching.md +0 -273
- data/docs/controller_action_aliases.md +0 -109
- data/docs/custom_lookup_chain.md +0 -48
- data/docs/custom_policy.md +0 -53
- data/docs/debugging.md +0 -55
- data/docs/decorators.md +0 -27
- data/docs/favicon.ico +0 -0
- data/docs/graphql.md +0 -302
- data/docs/i18n.md +0 -44
- data/docs/index.html +0 -43
- data/docs/instrumentation.md +0 -84
- data/docs/lookup_chain.md +0 -17
- data/docs/namespaces.md +0 -77
- data/docs/non_rails.md +0 -28
- data/docs/pre_checks.md +0 -57
- data/docs/pundit_migration.md +0 -80
- data/docs/quick_start.md +0 -118
- data/docs/rails.md +0 -120
- data/docs/reasons.md +0 -120
- data/docs/scoping.md +0 -255
- data/docs/testing.md +0 -333
- data/docs/writing_policies.md +0 -107
- data/gemfiles/jruby.gemfile +0 -8
- data/gemfiles/rails42.gemfile +0 -8
- data/gemfiles/rails6.gemfile +0 -8
- data/gemfiles/railsmaster.gemfile +0 -6
- data/lib/action_policy/ext/string_match.rb +0 -14
- data/lib/action_policy/ext/yield_self_then.rb +0 -25
data/docs/assets/vue.min.css
DELETED
@@ -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).
|
data/docs/behaviour.md
DELETED
@@ -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
|
-
```
|
data/docs/caching.md
DELETED
@@ -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
|
-
```
|