dude_policy 0.1.0 → 0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +6 -4
- data/Gemfile.lock +34 -25
- data/README.md +423 -15
- data/lib/dude_policy/nil_dude_policy.rb +6 -0
- data/lib/dude_policy/nil_extension.rb +4 -0
- data/lib/dude_policy/version.rb +1 -1
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 66aa43b5017af33bec8c323d9cba09e09e7f1537654dc462da4bcd20bc4e63ad
|
|
4
|
+
data.tar.gz: 7c937f1c730de57477f4062cf289d5d350de7e708f32b753fcde8401c4c8d896
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9ad555c4f1279d15c6c9dd3a171f4d575f9fa54b142a1e57819e7b1683d25bb5a350aaf669502daf827551642f057e5bd1777438f7db8cb63a3a9676f3982088
|
|
7
|
+
data.tar.gz: eb12c6da901ddd9ed616f027d023f3e8d2e7dd96dc02a350605d42dbbab6c654acae8fe3cc132058a3e0aff3b8756b2e7510d69dd4a0d260be7d8b42912d4632
|
data/.travis.yml
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,41 +1,50 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
dude_policy (0.1.
|
|
4
|
+
dude_policy (0.1.1)
|
|
5
5
|
activesupport (> 4.2)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
8
8
|
remote: https://rubygems.org/
|
|
9
9
|
specs:
|
|
10
|
-
activesupport (
|
|
10
|
+
activesupport (7.1.1)
|
|
11
|
+
base64
|
|
12
|
+
bigdecimal
|
|
11
13
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
connection_pool (>= 2.2.5)
|
|
15
|
+
drb
|
|
16
|
+
i18n (>= 1.6, < 2)
|
|
17
|
+
minitest (>= 5.1)
|
|
18
|
+
mutex_m
|
|
19
|
+
tzinfo (~> 2.0)
|
|
20
|
+
base64 (0.1.1)
|
|
21
|
+
bigdecimal (3.1.4)
|
|
22
|
+
concurrent-ruby (1.2.2)
|
|
23
|
+
connection_pool (2.4.1)
|
|
24
|
+
diff-lcs (1.5.0)
|
|
25
|
+
drb (2.1.1)
|
|
26
|
+
ruby2_keywords
|
|
27
|
+
i18n (1.14.1)
|
|
19
28
|
concurrent-ruby (~> 1.0)
|
|
20
|
-
minitest (5.
|
|
29
|
+
minitest (5.20.0)
|
|
30
|
+
mutex_m (0.1.2)
|
|
21
31
|
rake (12.3.3)
|
|
22
|
-
rspec (3.
|
|
23
|
-
rspec-core (~> 3.
|
|
24
|
-
rspec-expectations (~> 3.
|
|
25
|
-
rspec-mocks (~> 3.
|
|
26
|
-
rspec-core (3.
|
|
27
|
-
rspec-support (~> 3.
|
|
28
|
-
rspec-expectations (3.
|
|
32
|
+
rspec (3.12.0)
|
|
33
|
+
rspec-core (~> 3.12.0)
|
|
34
|
+
rspec-expectations (~> 3.12.0)
|
|
35
|
+
rspec-mocks (~> 3.12.0)
|
|
36
|
+
rspec-core (3.12.2)
|
|
37
|
+
rspec-support (~> 3.12.0)
|
|
38
|
+
rspec-expectations (3.12.3)
|
|
29
39
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
30
|
-
rspec-support (~> 3.
|
|
31
|
-
rspec-mocks (3.
|
|
40
|
+
rspec-support (~> 3.12.0)
|
|
41
|
+
rspec-mocks (3.12.6)
|
|
32
42
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
33
|
-
rspec-support (~> 3.
|
|
34
|
-
rspec-support (3.
|
|
35
|
-
|
|
36
|
-
tzinfo (
|
|
37
|
-
|
|
38
|
-
zeitwerk (2.3.0)
|
|
43
|
+
rspec-support (~> 3.12.0)
|
|
44
|
+
rspec-support (3.12.1)
|
|
45
|
+
ruby2_keywords (0.0.5)
|
|
46
|
+
tzinfo (2.0.6)
|
|
47
|
+
concurrent-ruby (~> 1.0)
|
|
39
48
|
|
|
40
49
|
PLATFORMS
|
|
41
50
|
ruby
|
data/README.md
CHANGED
|
@@ -5,14 +5,14 @@ from point of view of current_user/current_account (the **dude**)
|
|
|
5
5
|
|
|
6
6
|

|
|
7
7
|
|
|
8
|
-
Here are some examples what
|
|
8
|
+
Here are some examples what I mean:
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
```ruby
|
|
12
|
-
#
|
|
12
|
+
# rails console
|
|
13
13
|
article = Article.find(123)
|
|
14
14
|
review = Review.find(123)
|
|
15
|
-
current_user = User.find(432) # e.g. Devise
|
|
15
|
+
current_user = User.find(432) # e.g. Devise or any other authentication solution
|
|
16
16
|
|
|
17
17
|
current_user.dude.able_to_edit_article?(article)
|
|
18
18
|
# => true
|
|
@@ -22,6 +22,12 @@ current_user.dude.able_to_add_article_review?(article)
|
|
|
22
22
|
|
|
23
23
|
current_user.dude.able_to_delete_review?(review)
|
|
24
24
|
# => false
|
|
25
|
+
|
|
26
|
+
current_user.policy.able_to_view_articles?
|
|
27
|
+
# => true
|
|
28
|
+
|
|
29
|
+
current_user.policy.able_to_create_articles?
|
|
30
|
+
# => false
|
|
25
31
|
```
|
|
26
32
|
|
|
27
33
|
[RSpec](https://rspec.info/) examples:
|
|
@@ -34,15 +40,23 @@ RSpec.describe 'short demo' do
|
|
|
34
40
|
let(:different_user) { User.create }
|
|
35
41
|
|
|
36
42
|
# you write tests like this:
|
|
37
|
-
it { expect(author_user.able_to_edit_article?(article)).to be_truthy }
|
|
43
|
+
it { expect(author_user.dude.able_to_edit_article?(article)).to be_truthy }
|
|
38
44
|
|
|
39
45
|
# or you can take advantage of native `be_` RSpec matcher that converts any questionmark ending method to matcher
|
|
40
|
-
it { expect(author_user).to be_able_to_edit_article(article) }
|
|
41
|
-
it { expect(different_user).not_to be_able_to_edit_article(article) }
|
|
42
|
-
it { expect(author_user).not_to be_able_to_add_article_review(article) }
|
|
43
|
-
it { expect(different_user).to be_able_to_add_article_review(article) }
|
|
44
|
-
it { expect(author_user).not_to be_able_to_delete_review(article) }
|
|
45
|
-
it { expect(different_user).to be_able_to_add_article_review(article) }
|
|
46
|
+
it { expect(author_user.dude).to be_able_to_edit_article(article) }
|
|
47
|
+
it { expect(different_user.dude).not_to be_able_to_edit_article(article) }
|
|
48
|
+
it { expect(author_user.dude).not_to be_able_to_add_article_review(article) }
|
|
49
|
+
it { expect(different_user.dude).to be_able_to_add_article_review(article) }
|
|
50
|
+
it { expect(author_user.dude).not_to be_able_to_delete_review(article) }
|
|
51
|
+
it { expect(different_user.dude).to be_able_to_add_article_review(article) }
|
|
52
|
+
|
|
53
|
+
it { expect(author_user.policy).to be_able_to_view_articles? }
|
|
54
|
+
it { expect(author_user.policy).not_to be_able_to_create_articles? }
|
|
55
|
+
|
|
56
|
+
context "when paid subscription" do
|
|
57
|
+
before { author_user.update_attributes(subscription: "paid") }
|
|
58
|
+
it { expect(author_user.policy).to be_able_to_create_articles? }
|
|
59
|
+
end
|
|
46
60
|
end
|
|
47
61
|
```
|
|
48
62
|
|
|
@@ -69,10 +83,24 @@ class ArticlePolicy < DudePolicy::BasePolicy
|
|
|
69
83
|
end
|
|
70
84
|
```
|
|
71
85
|
|
|
72
|
-
|
|
86
|
+
```ruby
|
|
87
|
+
# app/policy/user_policy.rb
|
|
88
|
+
class UserPolicy < DudePolicy::BasePolicy
|
|
89
|
+
def able_to_view_articles?; true; end
|
|
90
|
+
|
|
91
|
+
def able_to_create_articles?
|
|
92
|
+
return true if resource.subscription == "paid"
|
|
93
|
+
false
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
> For more examples pls check the [example app](https://github.com/equivalent/dude_policy_example1)
|
|
73
99
|
|
|
74
100
|
## Installation
|
|
75
101
|
|
|
102
|
+
[](https://travis-ci.org/equivalent/dude_policy)
|
|
103
|
+
|
|
76
104
|
Add this line to your application's Gemfile:
|
|
77
105
|
|
|
78
106
|
```ruby
|
|
@@ -89,17 +117,397 @@ Or install it yourself as:
|
|
|
89
117
|
|
|
90
118
|
## Usage
|
|
91
119
|
|
|
92
|
-
|
|
120
|
+
Gem is responsible for **Authorization** (what a logged in user can/cannot do)
|
|
121
|
+
For **Authentication** (is User logged in ?) you will need a different solution / gem (e.g. [Devise](https://github.com/heartcombo/devise), custom login solution, ...)
|
|
122
|
+
|
|
123
|
+
Once you have your Authentication solution implemeted and `dude_policy`
|
|
124
|
+
gem installed create a directory `app/policy` in your Ruby on Rails
|
|
125
|
+
app.
|
|
126
|
+
|
|
127
|
+
> Note: Since Rails version 4 files in `app/anything` directories are autoloaded. So no additional magic is needed
|
|
128
|
+
|
|
129
|
+
There create your policy file:
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
# app/policy/article_policy
|
|
134
|
+
class ArticlePolicy < DudePolicy::BasePolicy
|
|
135
|
+
def able_to_update_article?(dude:)
|
|
136
|
+
return true if dude.admin?
|
|
137
|
+
return true if dude == resource.author
|
|
138
|
+
false
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
> Note: Policy should be name as the model suffixed with word "Policy". So if
|
|
145
|
+
> you have `ConstructiveComment` model your policy should be named `ConstructiveCommentPolicy` located in `app/policy/constructive_comment_policy.rb`
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
You also need to tell your models what role they play
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
class Article < ApplicationRecord
|
|
152
|
+
include DudePolicy::HasPolicy # will add a method `article.policy`
|
|
153
|
+
|
|
154
|
+
# ...
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
class User < ApplicationRecord
|
|
161
|
+
include DudePolicy::IsADude # will add a method `user.dude`
|
|
162
|
+
include DudePolicy::HasPolicy # will add a method `user.policy`
|
|
163
|
+
|
|
164
|
+
# ...
|
|
165
|
+
end
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
This way you will be able to call:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
user = User.find(123)
|
|
172
|
+
user.dude.able_to_update_article?(@article)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
> Note: same model can include both `DudePolicy::IsADude` and `DudePolicy::IsADude` but don't have to.
|
|
176
|
+
|
|
177
|
+
> Note: please be sure to check the [Philosophy](https://github.com/equivalent/dude_policy#philospophy) section of this README to fully understand the flow
|
|
178
|
+
|
|
179
|
+
This way you will be implement it in your application:
|
|
180
|
+
|
|
181
|
+
#### protect views
|
|
182
|
+
|
|
183
|
+
```erb
|
|
184
|
+
<%= link_to 'Edit', edit_article_path(@article) if current_user.dude.able_to_update_article?(@article) %>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
#### protect controllers
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
# app/controllers/articles_controller.rb
|
|
191
|
+
class ArticlesController < ApplicationController
|
|
192
|
+
# ...
|
|
193
|
+
|
|
194
|
+
def update
|
|
195
|
+
@article = Article.find_by(params[:id])
|
|
196
|
+
raise(DudePolicy::NotAuthorized) unless current_user.dude.able_to_update_article?(@article)
|
|
197
|
+
|
|
198
|
+
if @article.update(article_params)
|
|
199
|
+
redirect_to @article, notice: 'Article was successfully updated.'
|
|
200
|
+
else
|
|
201
|
+
render :edit
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
> gem provides error class `DudePolicy::NotAuthorized` so you
|
|
208
|
+
> can implement [rescue_from](https://apidock.com/rails/ActiveSupport/Rescuable/ClassMethods/rescue_from) logic around not authenticated
|
|
209
|
+
> scenarios. If you have no idea what I'm talking about pls check [example application code](https://github.com/equivalent/dude_policy_example1/blob/master/app/controllers/application_controller.rb)
|
|
210
|
+
|
|
211
|
+
#### protect business logic
|
|
212
|
+
|
|
213
|
+
There are cases when you want to protect your business logic that is
|
|
214
|
+
beyond MVC level. For example:
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
# app/polcy/user_policy.rb
|
|
218
|
+
class UserPolicy < DudePolicy::BasePolicy
|
|
219
|
+
def able_to_see_user_full_name?(dude:)
|
|
220
|
+
return true if dude.admin?
|
|
221
|
+
return true if dude == resource
|
|
222
|
+
false
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# app/helpers/application_helper.rb
|
|
228
|
+
def author_name(user)
|
|
229
|
+
return unless user
|
|
230
|
+
if current_user.dude.able_to_see_user_full_name?(user)
|
|
231
|
+
user.name
|
|
232
|
+
else
|
|
233
|
+
first_letter = user.name[0]
|
|
234
|
+
"#{first_letter}."
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
#### RSpec testing
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
##### policy test
|
|
243
|
+
|
|
244
|
+
You should be writing tests from perspective of current_user / current_account (the dude) and what roles they play.
|
|
245
|
+
|
|
246
|
+
If you have 2 roles (admin/regular user) test all policy methods for both
|
|
247
|
+
roles. If you have 8 roles (admin/moderator/client-manager/external-employee/noob/...) test all policy methods from all eight perspectives.
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
# spec/policy/article_policy_spec.rb
|
|
251
|
+
require 'rails_helper'
|
|
252
|
+
RSpec.describe ArticlePolicy do
|
|
253
|
+
let(:article) { create :article, author: author }
|
|
254
|
+
let(:author) { create :user }
|
|
255
|
+
|
|
256
|
+
context 'when nil user' do
|
|
257
|
+
let(:current_user) { nil }
|
|
258
|
+
|
|
259
|
+
it { expect(current_user.dude).not_to be_able_to_update_article(article) }
|
|
260
|
+
it { expect(current_user.dude).not_to be_able_to_delete_article(article) }
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
context 'when regular_user' do
|
|
264
|
+
let(:current_user) { create :user }
|
|
93
265
|
|
|
94
|
-
|
|
266
|
+
it { expect(current_user.dude).not_to be_able_to_update_article(article) }
|
|
267
|
+
it { expect(current_user.dude).not_to be_able_to_delete_article(article) }
|
|
268
|
+
|
|
269
|
+
context ' is author of article' do
|
|
270
|
+
let(:author) { current_user }
|
|
271
|
+
it { expect(current_user.dude).to be_able_to_update_article(article) }
|
|
272
|
+
it { expect(current_user.dude).to be_able_to_delete_article(article) }
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
context 'when admin' do
|
|
277
|
+
let(:current_user) { create :user, admin: true }
|
|
278
|
+
it { expect(current_user.dude).to be_able_to_update_article(article) }
|
|
279
|
+
it { expect(current_user.dude).not_to be_able_to_delete_article(article) }
|
|
280
|
+
|
|
281
|
+
context ' is author of article' do
|
|
282
|
+
let(:author) { current_user }
|
|
283
|
+
it { expect(current_user.dude).to be_able_to_update_article(article) }
|
|
284
|
+
it { expect(current_user.dude).to be_able_to_update_article(article) }
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
> note the be_*****() matcher is built in RSpec (no special magic in this gem). It's up to you if you prefere `expect(current_user.dude).to be_able_to_update_article(article)` or
|
|
291
|
+
> `expect(current_user.dude.able_to_update_article?(article)).to be_truthy`. Both are valid from point of native RSpec.
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
Do yourself a favor and **don't write low level Unit Tests** like `expect(ArticlePolicy.new(article)).to be_able_to_update_article(dude: current_user)` !
|
|
295
|
+
When it comes to policy tests this can lead to huge security disasters ([full explanation](https://blog.eq8.eu/assets/2019/unit-test.jpg))
|
|
296
|
+
|
|
297
|
+
##### request test
|
|
298
|
+
|
|
299
|
+
Now that we tested policy for every possible role of a user we can stub
|
|
300
|
+
the policy. We want to do it on the same interface level as we tested our policies
|
|
301
|
+
that means `allow(current_user.dude).to receive(:able_to_update_article?).and_return(true)`
|
|
302
|
+
|
|
303
|
+
> note: simmilar approach we would apply if you write controller RSpec test
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
require 'rails_helper'
|
|
308
|
+
RSpec.describe "Articles", type: :request do
|
|
309
|
+
let(:article) { create :article }
|
|
310
|
+
|
|
311
|
+
describe "put /articles/xxxx" do
|
|
312
|
+
def trigger_update
|
|
313
|
+
if current_user
|
|
314
|
+
# devise login
|
|
315
|
+
sign_in current_user
|
|
316
|
+
|
|
317
|
+
# policies are tested with `spec/policy/article_spec.rb so we can stub
|
|
318
|
+
allow(current_user.dude)
|
|
319
|
+
.to receive(:able_to_update_article?)
|
|
320
|
+
.with(article)
|
|
321
|
+
.and_return(authorized)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
put article_path(article), params: {format: :html, article: { title: 'cat' }}
|
|
325
|
+
article.reload
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
context 'when not authenticated' do
|
|
329
|
+
let(:current_user) { nil }
|
|
330
|
+
|
|
331
|
+
it { expect { trigger_update }.not_to change { article.title } }
|
|
332
|
+
it do
|
|
333
|
+
trigger_update
|
|
334
|
+
expect(response.status).to eq 302 # redirect by update
|
|
335
|
+
follow_redirect!
|
|
336
|
+
expect(response.body).to include('You need to sign in or sign up before continuing.')
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
context 'when authenticated' do
|
|
341
|
+
let(:current_user) { create :user }
|
|
342
|
+
|
|
343
|
+
context 'when not authorized' do
|
|
344
|
+
let(:authorized) { false }
|
|
345
|
+
|
|
346
|
+
it { expect { trigger_update }.not_to change { article.title } }
|
|
347
|
+
it do
|
|
348
|
+
trigger_update
|
|
349
|
+
expect(response.status).to eq 302 # redirect to root_path by `authenticate!` method is ApplicationController
|
|
350
|
+
follow_redirect!
|
|
351
|
+
expect(response.body).to include('Sorry current user is not authorized to perform this action')
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
context 'when authorized' do
|
|
356
|
+
let(:authorized) { true }
|
|
357
|
+
|
|
358
|
+
it { expect { trigger_update }.to change { article.title }.from('interesting article').to('cat') }
|
|
359
|
+
it do
|
|
360
|
+
trigger_update
|
|
361
|
+
expect(response.status).to eq 302
|
|
362
|
+
follow_redirect!
|
|
363
|
+
expect(response.body).to include('Article was successfully updated.')
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
#### More examples
|
|
373
|
+
|
|
374
|
+
> For more examples pls check the [example app](https://github.com/equivalent/dude_policy_example1)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
## Philosophy
|
|
378
|
+
|
|
379
|
+
I've spent many years and tone of time playing around with different Authorization
|
|
380
|
+
solutions and philosophies. All boils down to fact that [Policy Objects](https://blog.eq8.eu/article/policy-object.html) are the best you can implement.
|
|
381
|
+
|
|
382
|
+
Problem is that although there are decent policy object solutions usually
|
|
383
|
+
they are not specific enough on implementation strategy and teams/teammates still create a
|
|
384
|
+
mess.
|
|
385
|
+
|
|
386
|
+
So here are some core principles and their benefits:
|
|
387
|
+
|
|
388
|
+
#### Access policies from model interface methods
|
|
389
|
+
|
|
390
|
+
By accessing policy from `current_account.dude.able_to_do_something?(resource)` you'll
|
|
391
|
+
overcome multiple challenges:
|
|
392
|
+
|
|
393
|
+
* less chaos within the team
|
|
394
|
+
* easier for Junior Developers to jump on the project with just basic MVC skill set
|
|
395
|
+
* unified way how to write code and implementation of policies
|
|
396
|
+
* performance - you don't load same objects as they are memoized on `model.policy` level (check source code)
|
|
397
|
+
|
|
398
|
+
#### Stub policy in tests from perspective of current user (`current_account.dude`)
|
|
399
|
+
|
|
400
|
+
This may not be an issue if you are using general login solution gems
|
|
401
|
+
like Devise. But in custom login solutions you may find it difficult to stub underlying resouces in request/controller tests.
|
|
402
|
+
|
|
403
|
+
By writing everything from user/account perspective `allow(current_account.dude).to receive(...)` your stubs will have less maintenance headache
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
#### Unified naming of policy methods
|
|
407
|
+
|
|
408
|
+
Your policy objects are just simple Ruby objects so there is no
|
|
409
|
+
restriction to name your methods in in anything you want.
|
|
410
|
+
|
|
411
|
+
From experience I highly advise you to name the policy methods as
|
|
412
|
+
`able_to_` + `action` + `resources names`
|
|
413
|
+
|
|
414
|
+
example
|
|
415
|
+
|
|
416
|
+
```ruby
|
|
417
|
+
class ProductPolicy < DudePolicy::BasePolicy
|
|
418
|
+
def able_to_delete_product(dude:)
|
|
419
|
+
#...
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def able_to_add_product_review_comment(dude:)
|
|
423
|
+
#...
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
class ReviewCommentPolicy < DudePolicy::BasePolicy
|
|
428
|
+
|
|
429
|
+
def able_to_delete_review_comment(dude:)
|
|
430
|
+
#...
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
this way you will be able to take advantage of built in RSpec feature
|
|
436
|
+
and write tests like
|
|
437
|
+
|
|
438
|
+
`it { expect(current_account.dude).to be_able_to_delete_review_comment(review_comment) }`
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
Important thing is that **we write policy tests on the model method interface**
|
|
442
|
+
`current_account.dude` because we expect to stub them in
|
|
443
|
+
resource/controller test on the same interface.
|
|
444
|
+
|
|
445
|
+
> Avoid writing tests from perspective of `resource.policy` or `NameOfMyPolicy.new(current_account)`
|
|
446
|
+
|
|
447
|
+
#### Nil is a user too
|
|
448
|
+
|
|
449
|
+
Once you install gem you may notice that you are able to do
|
|
450
|
+
`nil.dude.can_do_anything? => false` and `nil.policy.can_do_anything? => false` this is a feature not a bug.
|
|
451
|
+
|
|
452
|
+
Sometimes your application need to deal with `nil` as current_user and
|
|
453
|
+
you don't want to have conditions `if current_user` all over the place.
|
|
454
|
+
That's why gem implements [Null Object Pattern](https://avdi.codes/null-objects-and-falsiness/) on `nil.dude` method that returns `false` all the time
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
#### Nothing new
|
|
458
|
+
|
|
459
|
+
The gem/lib is really tiny. Only dependency is Rails itself. If you want
|
|
460
|
+
to be vanilla Rails (no external gems) or implement this in other Ruby frameworks (e.g.
|
|
461
|
+
Sinatra) feel free to copy individual files from the `lib` directly
|
|
462
|
+
|
|
463
|
+
The whole gem is just [delegator](https://github.com/equivalent/dude_policy/blob/master/lib/dude_policy/dude.rb), [nil extensions](https://github.com/equivalent/dude_policy/blob/master/lib/dude_policy/nil_extension.rb), memoized model methods [1](https://github.com/equivalent/dude_policy/blob/master/lib/dude_policy/has_policy.rb) [2](https://github.com/equivalent/dude_policy/blob/master/lib/dude_policy/is_a_dude.rb) and your [Policy Objects](https://blog.eq8.eu/article/policy-object.html).
|
|
464
|
+
|
|
465
|
+
`PolicyObject + model method interface == Dude Policy gem` It's not a rocket science.
|
|
466
|
+
|
|
467
|
+
**The important part is the philosophy not the gem !**
|
|
468
|
+
|
|
469
|
+
## How it works
|
|
470
|
+
|
|
471
|
+
Core of the gem is the delegator that flips dependencies (e.g. `user.dude`)
|
|
472
|
+
|
|
473
|
+
So you
|
|
474
|
+
define `ArticlePolicy` that works with `article` and you pass `dude` as
|
|
475
|
+
a keyword argument:
|
|
476
|
+
|
|
477
|
+
```
|
|
478
|
+
class ArticlePolicy < DudePolicy::BasePolicy
|
|
479
|
+
def able_to_do_something_on_article?(dude:)
|
|
480
|
+
#...
|
|
481
|
+
# `dude' represent the user
|
|
482
|
+
#...
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
...by using `include PolicyDude::HasPolicy` your model have access to
|
|
488
|
+
this policy via method `article.policy`
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
The model responsible for representing current user (`User`) is able to
|
|
492
|
+
access the "flip dependency delegator" by using `include PolicyDude::IsADude` (e.g `user.dude`)
|
|
493
|
+
|
|
494
|
+
This allow us to call method of the argumet resource policy:
|
|
495
|
+
|
|
496
|
+
```
|
|
497
|
+
user.dude.able_to_do_something_on_article?(article) -> article.policy.able_to_do_something_on_article(dude: user)
|
|
498
|
+
user.dude.able_to_do_something_on_somethig_else?(product) -> product.policy.able_to_do_something_on_somethig_else(dude: user)
|
|
499
|
+
```
|
|
95
500
|
|
|
96
|
-
|
|
501
|
+
So in theory you could access the policy from resource point of view
|
|
502
|
+
`article.policy._____(dude: user)` but **don't do this** as the philosophy is **you should write your code from perspective of current_user**
|
|
503
|
+
( `user.dude.____(article)` )
|
|
97
504
|
|
|
98
505
|
|
|
506
|
+
> `model.policy` is exposed mainly for debugging level & performance (model level memoization)
|
|
99
507
|
|
|
100
508
|
## Contributing
|
|
101
509
|
|
|
102
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/equivalent/dude_policy This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/
|
|
510
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/equivalent/dude_policy This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/equivalent/dude_policy/blob/master/CODE_OF_CONDUCT.md).
|
|
103
511
|
|
|
104
512
|
|
|
105
513
|
## License
|
|
@@ -6,6 +6,12 @@ module DudePolicy
|
|
|
6
6
|
false
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
+
# test frameworks like RSpec test first if method responds to before calling it
|
|
10
|
+
# as this is a NullObject delegator it responds to any method call
|
|
11
|
+
def respond_to?(*)
|
|
12
|
+
true
|
|
13
|
+
end
|
|
14
|
+
|
|
9
15
|
def inspect
|
|
10
16
|
"<#DudePolicy##{object_id} on nil>"
|
|
11
17
|
end
|
data/lib/dude_policy/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dude_policy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: '0.2'
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tomas Valent
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2023-10-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -61,7 +61,7 @@ metadata:
|
|
|
61
61
|
homepage_uri: https://github.com/equivalent/dude_policy
|
|
62
62
|
source_code_uri: https://github.com/equivalent/dude_policy
|
|
63
63
|
changelog_uri: https://github.com/equivalent/dude_policy/blob/master/CHANGELOG.md
|
|
64
|
-
post_install_message:
|
|
64
|
+
post_install_message:
|
|
65
65
|
rdoc_options: []
|
|
66
66
|
require_paths:
|
|
67
67
|
- lib
|
|
@@ -76,8 +76,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
76
76
|
- !ruby/object:Gem::Version
|
|
77
77
|
version: '0'
|
|
78
78
|
requirements: []
|
|
79
|
-
rubygems_version: 3.
|
|
80
|
-
signing_key:
|
|
79
|
+
rubygems_version: 3.3.7
|
|
80
|
+
signing_key:
|
|
81
81
|
specification_version: 4
|
|
82
82
|
summary: Policy objects for Ruby on Rails from perspectvie of current account
|
|
83
83
|
test_files: []
|