action_policy 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rubocop.yml +85 -0
- data/.travis.yml +25 -2
- data/CHANGELOG.md +7 -0
- data/Gemfile +12 -3
- data/README.md +71 -12
- data/Rakefile +9 -1
- data/action_policy.gemspec +11 -5
- data/docs/.nojekyll +0 -0
- data/docs/CNAME +1 -0
- data/docs/README.md +46 -0
- data/docs/_sidebar.md +19 -0
- data/docs/aliases.md +54 -0
- data/docs/assets/docsify.min.js +1 -0
- data/docs/assets/fonts/FiraCode-Medium.woff +0 -0
- data/docs/assets/fonts/FiraCode-Regular.woff +0 -0
- data/docs/assets/images/cache.png +0 -0
- data/docs/assets/images/cache.svg +70 -0
- data/docs/assets/images/layer.png +0 -0
- data/docs/assets/images/layer.svg +92 -0
- data/docs/assets/prism-ruby.min.js +1 -0
- data/docs/assets/styles.css +317 -0
- data/docs/assets/vue.min.css +1 -0
- data/docs/authorization_context.md +33 -0
- data/docs/caching.md +262 -0
- data/docs/custom_lookup_chain.md +48 -0
- data/docs/custom_policy.md +51 -0
- data/docs/favicon.ico +0 -0
- data/docs/i18n.md +3 -0
- data/docs/index.html +25 -0
- data/docs/instrumentation.md +3 -0
- data/docs/lookup_chain.md +16 -0
- data/docs/namespaces.md +69 -0
- data/docs/non_rails.md +29 -0
- data/docs/pre_checks.md +57 -0
- data/docs/quick_start.md +102 -0
- data/docs/rails.md +110 -0
- data/docs/reasons.md +67 -0
- data/docs/testing.md +116 -0
- data/docs/writing_policies.md +55 -0
- data/gemfiles/jruby.gemfile +5 -0
- data/gemfiles/rails42.gemfile +5 -0
- data/gemfiles/railsmaster.gemfile +6 -0
- data/lib/action_policy.rb +34 -2
- data/lib/action_policy/authorizer.rb +28 -0
- data/lib/action_policy/base.rb +24 -0
- data/lib/action_policy/behaviour.rb +94 -0
- data/lib/action_policy/behaviours/memoized.rb +56 -0
- data/lib/action_policy/behaviours/namespaced.rb +80 -0
- data/lib/action_policy/behaviours/policy_for.rb +23 -0
- data/lib/action_policy/behaviours/thread_memoized.rb +54 -0
- data/lib/action_policy/ext/module_namespace.rb +21 -0
- data/lib/action_policy/ext/policy_cache_key.rb +67 -0
- data/lib/action_policy/ext/string_constantize.rb +23 -0
- data/lib/action_policy/lookup_chain.rb +84 -0
- data/lib/action_policy/policy/aliases.rb +69 -0
- data/lib/action_policy/policy/authorization.rb +91 -0
- data/lib/action_policy/policy/cache.rb +74 -0
- data/lib/action_policy/policy/cached_apply.rb +28 -0
- data/lib/action_policy/policy/core.rb +64 -0
- data/lib/action_policy/policy/defaults.rb +37 -0
- data/lib/action_policy/policy/pre_check.rb +210 -0
- data/lib/action_policy/policy/reasons.rb +109 -0
- data/lib/action_policy/rails/channel.rb +15 -0
- data/lib/action_policy/rails/controller.rb +90 -0
- data/lib/action_policy/railtie.rb +74 -0
- data/lib/action_policy/rspec.rb +3 -0
- data/lib/action_policy/rspec/be_authorized_to.rb +93 -0
- data/lib/action_policy/rspec/pundit_syntax.rb +48 -0
- data/lib/action_policy/test_helper.rb +46 -0
- data/lib/action_policy/testing.rb +64 -0
- data/lib/action_policy/version.rb +3 -1
- metadata +115 -9
@@ -0,0 +1 @@
|
|
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}
|
@@ -0,0 +1,33 @@
|
|
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 `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
|
+
|
23
|
+
To do that automatically in your `authorize!` and `allowed_to?` calls, you must also configure authorization context. For example, in your controller:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
class ApplicationController < ActionController::Base
|
27
|
+
# First argument should be the same as in the policy.
|
28
|
+
# `through` specifies the method name to be called to
|
29
|
+
# get the required context object
|
30
|
+
# (equals to the context name itself by default, i.e. `account`)
|
31
|
+
authorize :account, through: :current_account
|
32
|
+
end
|
33
|
+
```
|
data/docs/caching.md
ADDED
@@ -0,0 +1,262 @@
|
|
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
|
+
## Rule cache
|
99
|
+
|
100
|
+
### Per-instance
|
101
|
+
|
102
|
+
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)).
|
103
|
+
|
104
|
+
In that case, Action Policy invokes the rule method only once, remembers the result, and returns it immediately for the subsequent calls.
|
105
|
+
|
106
|
+
**NOTE**: rule results memoization is available only if you inherit from `ActionPolicy::Base` or include `ActionPolicy::Policy::CachedApply` into your `ApplicationPolicy`.
|
107
|
+
|
108
|
+
### Using the cache store
|
109
|
+
|
110
|
+
Some policy rules might be _performance-heavy_, e.g., make complex database queries.
|
111
|
+
|
112
|
+
In that case, it makes sense to cache the rule application result for a long time (not just for the duration of a request).
|
113
|
+
|
114
|
+
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:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
class StagePolicy < ApplicationPolicy
|
118
|
+
# mark show? rule to be cached
|
119
|
+
cache :show?
|
120
|
+
# you can also provide store-specific options
|
121
|
+
# cache :show?, expires_in: 1.hour
|
122
|
+
|
123
|
+
def show?
|
124
|
+
full_access? ||
|
125
|
+
user.stage_permissions.where(
|
126
|
+
stage_id: record.id
|
127
|
+
).exists?
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def full_access?
|
133
|
+
!record.funnel.is_private? ||
|
134
|
+
user.permissions
|
135
|
+
.where(
|
136
|
+
funnel_id: record.funnel_id,
|
137
|
+
full_access: true
|
138
|
+
).exists?
|
139
|
+
end
|
140
|
+
end
|
141
|
+
```
|
142
|
+
|
143
|
+
You must configure a cache store to use this feature:
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
ActionPolicy.cache_store = MyCacheStore.new
|
147
|
+
```
|
148
|
+
|
149
|
+
Or, in Rails:
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
# config/application.rb (or config/environments/<environment>.rb)
|
153
|
+
Rails.application.configure do |config|
|
154
|
+
config.action_policy.cache_store = :redis_cache_store
|
155
|
+
end
|
156
|
+
```
|
157
|
+
|
158
|
+
Cache store must provide at least a `#fetch(key, **options, &block)` method.
|
159
|
+
|
160
|
+
By default, Action Policy builds a cache key using the following scheme:
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
"#{cache_namespace}/#{context_cache_key}" \
|
164
|
+
"/#{record.policy_cache_key}/#{policy.class.name}/#{rule}"
|
165
|
+
```
|
166
|
+
|
167
|
+
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).
|
168
|
+
|
169
|
+
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.
|
170
|
+
|
171
|
+
You can define your own `cache_key` / `cache_namespace` / `context_cache_key` methods for policy class to override this logic.
|
172
|
+
|
173
|
+
#### Invalidation
|
174
|
+
|
175
|
+
There no one-size-fits-all solution for invalidation. It highly depends on your business logic.
|
176
|
+
|
177
|
+
**Case \#1**: no invalidation required.
|
178
|
+
|
179
|
+
First of all, you should try to avoid manual invalidation at all. That could be achieved by using elaborate cache keys.
|
180
|
+
|
181
|
+
Let's consider an example.
|
182
|
+
|
183
|
+
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`).
|
184
|
+
|
185
|
+
Then you can do the following:
|
186
|
+
- Keep tracking the last `Access` added/updated/deleted for resource (e.g. `Access.belongs_to :accessessable, touch: :access_updated_at`)
|
187
|
+
- Use the following cache keys:
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
class User
|
191
|
+
def policy_cache_key
|
192
|
+
"user::#{id}::#{role_id}"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
class Resource
|
197
|
+
def policy_cache_key
|
198
|
+
"#{resource.class.name}::#{id}::#{access_updated_at}"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
```
|
202
|
+
|
203
|
+
**Case \#2**: discarding all cache at once.
|
204
|
+
|
205
|
+
That's pretty easy: just override `cache_namespace` method in your `ApplicationPolicy` with the new value:
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
class ApplicationPolicy < ActionPolicy::Base
|
209
|
+
# It's a good idea to store the changing part in the constant
|
210
|
+
CACHE_VERSION = "v2".freeze
|
211
|
+
|
212
|
+
# or even from the env variable
|
213
|
+
# CACHE_VERSION = ENV.fetch("POLICY_CACHE_VERSION", "v2").freeze
|
214
|
+
|
215
|
+
def cache_namespace
|
216
|
+
"action_policy::#{CACHE_VERSION}"
|
217
|
+
end
|
218
|
+
end
|
219
|
+
```
|
220
|
+
|
221
|
+
**Case \#3**: discarding some keys.
|
222
|
+
|
223
|
+
That is an alternative approach to _crafting_ cache keys.
|
224
|
+
|
225
|
+
If you have a limited number of places in your application where you update access control,
|
226
|
+
you can invalidate policies cache manually. If your cache store supports `delete_matched` command (deleting keys using a wildcard), you can try the following:
|
227
|
+
|
228
|
+
```ruby
|
229
|
+
class ApplicationPolicy < ActionPolicy::Base
|
230
|
+
# Define custom cache key generator
|
231
|
+
def cache_key(rule)
|
232
|
+
"policy_cache/#{user.id}/#{self.class.name}/#{record.id}/#{rule}"
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
class Access < ApplicationRecord
|
237
|
+
belongs_to :resource
|
238
|
+
belongs_to :user
|
239
|
+
|
240
|
+
after_commit :cleanup_policy_cache, on: [:create, :destroy]
|
241
|
+
|
242
|
+
def cleanup_policy_cache
|
243
|
+
# Clear cache for the corresponding user-record pair
|
244
|
+
ActionPolicy.cache_store.delete_matched(
|
245
|
+
"policy_cache/#{user_id}/#{ResourcePolicy.name}/#{resource_id}/*"
|
246
|
+
)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
class User < ApplicationRecord
|
251
|
+
belongs_to :role
|
252
|
+
|
253
|
+
after_commit :cleanup_policy_cache, on: [:update], if: :role_id_changed?
|
254
|
+
|
255
|
+
def cleanup_policy_cache
|
256
|
+
# Clear all policies cache for user
|
257
|
+
ActionPolicy.cache_store.delete_matched(
|
258
|
+
"policy_cache/#{user_id}/*"
|
259
|
+
)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
```
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# Custom Lookup Chain
|
2
|
+
|
3
|
+
Action Policy's lookup chain is just an array of _probes_ (lambdas with a specific interface).
|
4
|
+
|
5
|
+
The lookup process itself is pretty simple:
|
6
|
+
- Call the first probe;
|
7
|
+
- Return the result if it is not `nil`;
|
8
|
+
- Go to the next probe.
|
9
|
+
|
10
|
+
You can override the default chain with your own. For example:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
ActionPolicy::LookupChain.chain = [
|
14
|
+
# Probe accepts record as the first argument
|
15
|
+
# and arbitrary options (passed to `authorize!` / `allowed_to?` call)
|
16
|
+
lambda do |record, **options|
|
17
|
+
# your custom lookup logic
|
18
|
+
end
|
19
|
+
]
|
20
|
+
```
|
21
|
+
|
22
|
+
## NullPolicy example
|
23
|
+
|
24
|
+
Let's consider a simple example of extending the existing lookup chain with one more probe.
|
25
|
+
|
26
|
+
Suppose that we want to have a fallback policy (policy used when none found for the resource) instead of raising an `ActionPolicy::NotFound` error.
|
27
|
+
|
28
|
+
Let's call this policy a `NullPolicy`:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
class NullPolicy < ActionPolicy::Base
|
32
|
+
default_rule :any?
|
33
|
+
|
34
|
+
def any?
|
35
|
+
false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
Here we use the [default rule](aliases.md#default-rule) to handle any rule applied.
|
41
|
+
|
42
|
+
Now we need to add a simple probe to the end of our lookup chain:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
ActionPolicy::LookupChain.chain << ->(_, _) { NullPolicy }
|
46
|
+
```
|
47
|
+
|
48
|
+
That's it!
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# Custom Base Policy
|
2
|
+
|
3
|
+
`ActionPolicy::Base` is a combination of all available policy extensions with the default configuration.
|
4
|
+
|
5
|
+
It looks like this:
|
6
|
+
|
7
|
+
<span style="display:none;"># rubocop:disable Style/ClassAndModuleChildren</span>
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
class ActionPolicy::Base
|
11
|
+
include ActionPolicy::Policy::Core
|
12
|
+
include ActionPolicy::Policy::Authorization
|
13
|
+
include ActionPolicy::Policy::Reasons
|
14
|
+
include ActionPolicy::Policy::PreCheck
|
15
|
+
include ActionPolicy::Policy::Aliases
|
16
|
+
include ActionPolicy::Policy::CachedApply
|
17
|
+
|
18
|
+
# ActionPolicy::Policy::Defaults module adds the following
|
19
|
+
|
20
|
+
authorize :user
|
21
|
+
|
22
|
+
default_rule :manage?
|
23
|
+
alias_rule :new?, to: :create?
|
24
|
+
|
25
|
+
def index?
|
26
|
+
false
|
27
|
+
end
|
28
|
+
|
29
|
+
def create?
|
30
|
+
false
|
31
|
+
end
|
32
|
+
|
33
|
+
def manage?
|
34
|
+
false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
```
|
38
|
+
|
39
|
+
<span style="display:none;"># rubocop:enable Style/ClassAndModuleChildren</span>
|
40
|
+
|
41
|
+
You can write your `ApplicationPolicy` from scratch instead of inheriting from `ActionPolicy::Base`
|
42
|
+
if the defaults above do not fit your needs. The only required component is `ActionPolicy::Policy::Core`:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
# minimal ApplicationPolicy
|
46
|
+
class ApplicationPolicy
|
47
|
+
include ActionPolicy::Policy::Core
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
The `Core` module provides `apply` and `allowed_to?` methods.
|
data/docs/favicon.ico
ADDED
Binary file
|
data/docs/i18n.md
ADDED
data/docs/index.html
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<title>Action Policy: authorization framework for Ruby/Rails applications</title>
|
6
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
7
|
+
<meta name="description" content="Description">
|
8
|
+
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
9
|
+
<link rel="stylesheet" href="assets/vue.min.css">
|
10
|
+
<link rel="stylesheet" href="assets/styles.css">
|
11
|
+
</head>
|
12
|
+
<body>
|
13
|
+
<div id="app"></div>
|
14
|
+
<script>
|
15
|
+
window.$docsify = {
|
16
|
+
name: 'action_policy',
|
17
|
+
repo: 'https://github.com/palkan/action_policy',
|
18
|
+
loadSidebar: true,
|
19
|
+
subMaxLevel: 3
|
20
|
+
}
|
21
|
+
</script>
|
22
|
+
<script src="assets/docsify.min.js"></script>
|
23
|
+
<script src="assets/prism-ruby.min.js"></script>
|
24
|
+
</body>
|
25
|
+
</html>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# Policy Lookup
|
2
|
+
|
3
|
+
Action Policy tries to automatically infer policy class from the target using the following _probes_:
|
4
|
+
|
5
|
+
1. If the target responds to `policy_class`, then use it;
|
6
|
+
2. If the target's class responds to `policy_class`, then use it;
|
7
|
+
3. If the target's class responds to `policy_name`, then use `#{target.class.policy_name}Policy`;
|
8
|
+
4. Otherwise, use `#{target.class.name}Policy`.
|
9
|
+
|
10
|
+
> \* [Namespaces](namespaces.md) could be also be considered when `namespace` option is set.
|
11
|
+
|
12
|
+
You can call `ActionPolicy.lookup(record, options)` to infer policy class for the record.
|
13
|
+
|
14
|
+
When no policy class is found, an `ActionPolicy::NotFound` error is raised.
|
15
|
+
|
16
|
+
You can [customize lookup](custom_lookup_chain.md) logic if necessary.
|