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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea59aff3b035ce29f5f37fae1b57ff7fa4a7a64626dfde17ff03265877612aad
4
- data.tar.gz: 9060298e426595b6b91564a39041101937b50212b9389f5aee1d1a9f931af223
3
+ metadata.gz: 66aa43b5017af33bec8c323d9cba09e09e7f1537654dc462da4bcd20bc4e63ad
4
+ data.tar.gz: 7c937f1c730de57477f4062cf289d5d350de7e708f32b753fcde8401c4c8d896
5
5
  SHA512:
6
- metadata.gz: d302e8aa34fc76f754692d10177c22a66c73d0605cf329877bbe16cec1df6c316225bbfc001c011b93df80d1eef3faf5605acf96464063b8c978b5f662e095ad
7
- data.tar.gz: 2d082b2f92376f1230c2b6c637a28a8733bf3c89cbc03e916f68a8128522d087e4b9432e650dad89271e27fd620e9f4adcb20a9101b75e1896decc1fe3dc0769
6
+ metadata.gz: 9ad555c4f1279d15c6c9dd3a171f4d575f9fa54b142a1e57819e7b1683d25bb5a350aaf669502daf827551642f057e5bd1777438f7db8cb63a3a9676f3982088
7
+ data.tar.gz: eb12c6da901ddd9ed616f027d023f3e8d2e7dd96dc02a350605d42dbbab6c654acae8fe3cc132058a3e0aff3b8756b2e7510d69dd4a0d260be7d8b42912d4632
data/.travis.yml CHANGED
@@ -1,6 +1,8 @@
1
- ---
2
1
  language: ruby
3
- cache: bundler
4
2
  rvm:
5
- - 2.6.3
6
- before_install: gem install bundler -v 2.1.1
3
+ - 2.5.3
4
+ - 2.6.5
5
+ - 2.7.0
6
+ before_install:
7
+ - yes | gem update --system --force
8
+ - gem install bundler
data/Gemfile.lock CHANGED
@@ -1,41 +1,50 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dude_policy (0.1.0)
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 (6.0.2.2)
10
+ activesupport (7.1.1)
11
+ base64
12
+ bigdecimal
11
13
  concurrent-ruby (~> 1.0, >= 1.0.2)
12
- i18n (>= 0.7, < 2)
13
- minitest (~> 5.1)
14
- tzinfo (~> 1.1)
15
- zeitwerk (~> 2.2)
16
- concurrent-ruby (1.1.6)
17
- diff-lcs (1.3)
18
- i18n (1.8.2)
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.14.0)
29
+ minitest (5.20.0)
30
+ mutex_m (0.1.2)
21
31
  rake (12.3.3)
22
- rspec (3.9.0)
23
- rspec-core (~> 3.9.0)
24
- rspec-expectations (~> 3.9.0)
25
- rspec-mocks (~> 3.9.0)
26
- rspec-core (3.9.1)
27
- rspec-support (~> 3.9.1)
28
- rspec-expectations (3.9.1)
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.9.0)
31
- rspec-mocks (3.9.1)
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.9.0)
34
- rspec-support (3.9.2)
35
- thread_safe (0.3.6)
36
- tzinfo (1.2.7)
37
- thread_safe (~> 0.1)
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 on any authentication solution
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
+ [![Build Status](https://travis-ci.org/equivalent/dude_policy.svg?branch=master)](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` a directory in your Ruby on Rails app
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
- And create your policy file:
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 "Philosophy" section of this README to fully understand the flow
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
- <td><%= link_to 'Edit', edit_article_path(article) if current_user.dude.able_to_update_article?(article) %></td>
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 policiese for every
213
- role. If you have 8 roles (admin/moderator/client-manager/external-employee/noob/...) test every method from their perspective.
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 in on same interface as we tested our policies
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/[USERNAME]/dude_policy/blob/master/CODE_OF_CONDUCT.md).
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
@@ -3,5 +3,9 @@ module DudePolicy
3
3
  def dude
4
4
  DudePolicy::NilDudePolicy.instance
5
5
  end
6
+
7
+ def policy
8
+ DudePolicy::NilDudePolicy.instance
9
+ end
6
10
  end
7
11
  end
@@ -1,3 +1,3 @@
1
1
  module DudePolicy
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2"
3
3
  end
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.1.1
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: 2020-04-13 00:00:00.000000000 Z
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.0.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: []