dude_policy 0.1.1 → 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 +188 -20
- 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
@@ -12,7 +12,7 @@ Here are some examples what I mean:
|
|
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
|
|
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
|
+
|
72
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
|
@@ -93,9 +121,12 @@ Gem is responsible for **Authorization** (what a logged in user can/cannot do)
|
|
93
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, ...)
|
94
122
|
|
95
123
|
Once you have your Authentication solution implemeted and `dude_policy`
|
96
|
-
gem installed create `app/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
|
97
128
|
|
98
|
-
|
129
|
+
There create your policy file:
|
99
130
|
|
100
131
|
|
101
132
|
```ruby
|
@@ -109,7 +140,6 @@ class ArticlePolicy < DudePolicy::BasePolicy
|
|
109
140
|
end
|
110
141
|
```
|
111
142
|
|
112
|
-
> Note: Since Rails version 4 files in `app/anything` directories are autoloaded. So no additional magic is needed
|
113
143
|
|
114
144
|
> Note: Policy should be name as the model suffixed with word "Policy". So if
|
115
145
|
> you have `ConstructiveComment` model your policy should be named `ConstructiveCommentPolicy` located in `app/policy/constructive_comment_policy.rb`
|
@@ -119,7 +149,7 @@ You also need to tell your models what role they play
|
|
119
149
|
|
120
150
|
```ruby
|
121
151
|
class Article < ApplicationRecord
|
122
|
-
include DudePolicy::HasPolicy
|
152
|
+
include DudePolicy::HasPolicy # will add a method `article.policy`
|
123
153
|
|
124
154
|
# ...
|
125
155
|
end
|
@@ -128,8 +158,8 @@ end
|
|
128
158
|
|
129
159
|
```ruby
|
130
160
|
class User < ApplicationRecord
|
131
|
-
include DudePolicy::IsADude
|
132
|
-
include DudePolicy::HasPolicy
|
161
|
+
include DudePolicy::IsADude # will add a method `user.dude`
|
162
|
+
include DudePolicy::HasPolicy # will add a method `user.policy`
|
133
163
|
|
134
164
|
# ...
|
135
165
|
end
|
@@ -144,14 +174,14 @@ user.dude.able_to_update_article?(@article)
|
|
144
174
|
|
145
175
|
> Note: same model can include both `DudePolicy::IsADude` and `DudePolicy::IsADude` but don't have to.
|
146
176
|
|
147
|
-
> Note: please be sure to check the
|
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
|
148
178
|
|
149
179
|
This way you will be implement it in your application:
|
150
180
|
|
151
181
|
#### protect views
|
152
182
|
|
153
183
|
```erb
|
154
|
-
|
184
|
+
<%= link_to 'Edit', edit_article_path(@article) if current_user.dude.able_to_update_article?(@article) %>
|
155
185
|
```
|
156
186
|
|
157
187
|
#### protect controllers
|
@@ -174,6 +204,10 @@ class ArticlesController < ApplicationController
|
|
174
204
|
end
|
175
205
|
```
|
176
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
|
+
|
177
211
|
#### protect business logic
|
178
212
|
|
179
213
|
There are cases when you want to protect your business logic that is
|
@@ -209,8 +243,8 @@ end
|
|
209
243
|
|
210
244
|
You should be writing tests from perspective of current_user / current_account (the dude) and what roles they play.
|
211
245
|
|
212
|
-
If you have 2 roles (admin/regular user) test all
|
213
|
-
|
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.
|
214
248
|
|
215
249
|
```ruby
|
216
250
|
# spec/policy/article_policy_spec.rb
|
@@ -253,6 +287,9 @@ RSpec.describe ArticlePolicy do
|
|
253
287
|
end
|
254
288
|
```
|
255
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
|
+
|
256
293
|
|
257
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)` !
|
258
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))
|
@@ -260,7 +297,7 @@ When it comes to policy tests this can lead to huge security disasters ([full ex
|
|
260
297
|
##### request test
|
261
298
|
|
262
299
|
Now that we tested policy for every possible role of a user we can stub
|
263
|
-
the policy. We want to do
|
300
|
+
the policy. We want to do it on the same interface level as we tested our policies
|
264
301
|
that means `allow(current_user.dude).to receive(:able_to_update_article?).and_return(true)`
|
265
302
|
|
266
303
|
> note: simmilar approach we would apply if you write controller RSpec test
|
@@ -337,9 +374,140 @@ end
|
|
337
374
|
> For more examples pls check the [example app](https://github.com/equivalent/dude_policy_example1)
|
338
375
|
|
339
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
|
+
```
|
500
|
+
|
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)` )
|
504
|
+
|
505
|
+
|
506
|
+
> `model.policy` is exposed mainly for debugging level & performance (model level memoization)
|
507
|
+
|
340
508
|
## Contributing
|
341
509
|
|
342
|
-
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).
|
343
511
|
|
344
512
|
|
345
513
|
## License
|
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: []
|