action_policy 0.0.1 → 0.1.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 +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.
|