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.
Files changed (73) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +85 -0
  3. data/.travis.yml +25 -2
  4. data/CHANGELOG.md +7 -0
  5. data/Gemfile +12 -3
  6. data/README.md +71 -12
  7. data/Rakefile +9 -1
  8. data/action_policy.gemspec +11 -5
  9. data/docs/.nojekyll +0 -0
  10. data/docs/CNAME +1 -0
  11. data/docs/README.md +46 -0
  12. data/docs/_sidebar.md +19 -0
  13. data/docs/aliases.md +54 -0
  14. data/docs/assets/docsify.min.js +1 -0
  15. data/docs/assets/fonts/FiraCode-Medium.woff +0 -0
  16. data/docs/assets/fonts/FiraCode-Regular.woff +0 -0
  17. data/docs/assets/images/cache.png +0 -0
  18. data/docs/assets/images/cache.svg +70 -0
  19. data/docs/assets/images/layer.png +0 -0
  20. data/docs/assets/images/layer.svg +92 -0
  21. data/docs/assets/prism-ruby.min.js +1 -0
  22. data/docs/assets/styles.css +317 -0
  23. data/docs/assets/vue.min.css +1 -0
  24. data/docs/authorization_context.md +33 -0
  25. data/docs/caching.md +262 -0
  26. data/docs/custom_lookup_chain.md +48 -0
  27. data/docs/custom_policy.md +51 -0
  28. data/docs/favicon.ico +0 -0
  29. data/docs/i18n.md +3 -0
  30. data/docs/index.html +25 -0
  31. data/docs/instrumentation.md +3 -0
  32. data/docs/lookup_chain.md +16 -0
  33. data/docs/namespaces.md +69 -0
  34. data/docs/non_rails.md +29 -0
  35. data/docs/pre_checks.md +57 -0
  36. data/docs/quick_start.md +102 -0
  37. data/docs/rails.md +110 -0
  38. data/docs/reasons.md +67 -0
  39. data/docs/testing.md +116 -0
  40. data/docs/writing_policies.md +55 -0
  41. data/gemfiles/jruby.gemfile +5 -0
  42. data/gemfiles/rails42.gemfile +5 -0
  43. data/gemfiles/railsmaster.gemfile +6 -0
  44. data/lib/action_policy.rb +34 -2
  45. data/lib/action_policy/authorizer.rb +28 -0
  46. data/lib/action_policy/base.rb +24 -0
  47. data/lib/action_policy/behaviour.rb +94 -0
  48. data/lib/action_policy/behaviours/memoized.rb +56 -0
  49. data/lib/action_policy/behaviours/namespaced.rb +80 -0
  50. data/lib/action_policy/behaviours/policy_for.rb +23 -0
  51. data/lib/action_policy/behaviours/thread_memoized.rb +54 -0
  52. data/lib/action_policy/ext/module_namespace.rb +21 -0
  53. data/lib/action_policy/ext/policy_cache_key.rb +67 -0
  54. data/lib/action_policy/ext/string_constantize.rb +23 -0
  55. data/lib/action_policy/lookup_chain.rb +84 -0
  56. data/lib/action_policy/policy/aliases.rb +69 -0
  57. data/lib/action_policy/policy/authorization.rb +91 -0
  58. data/lib/action_policy/policy/cache.rb +74 -0
  59. data/lib/action_policy/policy/cached_apply.rb +28 -0
  60. data/lib/action_policy/policy/core.rb +64 -0
  61. data/lib/action_policy/policy/defaults.rb +37 -0
  62. data/lib/action_policy/policy/pre_check.rb +210 -0
  63. data/lib/action_policy/policy/reasons.rb +109 -0
  64. data/lib/action_policy/rails/channel.rb +15 -0
  65. data/lib/action_policy/rails/controller.rb +90 -0
  66. data/lib/action_policy/railtie.rb +74 -0
  67. data/lib/action_policy/rspec.rb +3 -0
  68. data/lib/action_policy/rspec/be_authorized_to.rb +93 -0
  69. data/lib/action_policy/rspec/pundit_syntax.rb +48 -0
  70. data/lib/action_policy/test_helper.rb +46 -0
  71. data/lib/action_policy/testing.rb +64 -0
  72. data/lib/action_policy/version.rb +3 -1
  73. 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
+ ```
@@ -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.
Binary file
@@ -0,0 +1,3 @@
1
+ # I18n Support
2
+
3
+ 🛠 **WORK IN PROGRESS**
@@ -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,3 @@
1
+ # Instrumentation
2
+
3
+ 🛠 **WORK IN PROGRESS**
@@ -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.