dude_policy 0.1.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
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: []