dude_policy 0.1.0 → 0.1.1
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/README.md +246 -6
- data/lib/dude_policy/nil_dude_policy.rb +6 -0
- data/lib/dude_policy/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ea59aff3b035ce29f5f37fae1b57ff7fa4a7a64626dfde17ff03265877612aad
|
4
|
+
data.tar.gz: 9060298e426595b6b91564a39041101937b50212b9389f5aee1d1a9f931af223
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d302e8aa34fc76f754692d10177c22a66c73d0605cf329877bbe16cec1df6c316225bbfc001c011b93df80d1eef3faf5605acf96464063b8c978b5f662e095ad
|
7
|
+
data.tar.gz: 2d082b2f92376f1230c2b6c637a28a8733bf3c89cbc03e916f68a8128522d087e4b9432e650dad89271e27fd620e9f4adcb20a9101b75e1896decc1fe3dc0769
|
data/README.md
CHANGED
@@ -5,11 +5,11 @@ 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
15
|
current_user = User.find(432) # e.g. Devise on any authentication solution
|
@@ -69,7 +69,7 @@ class ArticlePolicy < DudePolicy::BasePolicy
|
|
69
69
|
end
|
70
70
|
```
|
71
71
|
|
72
|
-
|
72
|
+
> For more examples pls check the [example app](https://github.com/equivalent/dude_policy_example1)
|
73
73
|
|
74
74
|
## Installation
|
75
75
|
|
@@ -89,13 +89,253 @@ Or install it yourself as:
|
|
89
89
|
|
90
90
|
## Usage
|
91
91
|
|
92
|
-
|
92
|
+
Gem is responsible for **Authorization** (what a logged in user can/cannot do)
|
93
|
+
For **Authentication** (is User logged in ?) you will need a different solution / gem (e.g. [Devise](https://github.com/heartcombo/devise), custom login solution, ...)
|
93
94
|
|
94
|
-
|
95
|
+
Once you have your Authentication solution implemeted and `dude_policy`
|
96
|
+
gem installed create `app/policy` a directory in your Ruby on Rails app
|
95
97
|
|
96
|
-
|
98
|
+
And create your policy file:
|
97
99
|
|
98
100
|
|
101
|
+
```ruby
|
102
|
+
# app/policy/article_policy
|
103
|
+
class ArticlePolicy < DudePolicy::BasePolicy
|
104
|
+
def able_to_update_article?(dude:)
|
105
|
+
return true if dude.admin?
|
106
|
+
return true if dude == resource.author
|
107
|
+
false
|
108
|
+
end
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
112
|
+
> Note: Since Rails version 4 files in `app/anything` directories are autoloaded. So no additional magic is needed
|
113
|
+
|
114
|
+
> Note: Policy should be name as the model suffixed with word "Policy". So if
|
115
|
+
> you have `ConstructiveComment` model your policy should be named `ConstructiveCommentPolicy` located in `app/policy/constructive_comment_policy.rb`
|
116
|
+
|
117
|
+
|
118
|
+
You also need to tell your models what role they play
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
class Article < ApplicationRecord
|
122
|
+
include DudePolicy::HasPolicy
|
123
|
+
|
124
|
+
# ...
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
class User < ApplicationRecord
|
131
|
+
include DudePolicy::IsADude
|
132
|
+
include DudePolicy::HasPolicy
|
133
|
+
|
134
|
+
# ...
|
135
|
+
end
|
136
|
+
```
|
137
|
+
|
138
|
+
This way you will be able to call:
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
user = User.find(123)
|
142
|
+
user.dude.able_to_update_article?(@article)
|
143
|
+
```
|
144
|
+
|
145
|
+
> Note: same model can include both `DudePolicy::IsADude` and `DudePolicy::IsADude` but don't have to.
|
146
|
+
|
147
|
+
> Note: please be sure to check the "Philosophy" section of this README to fully understand the flow
|
148
|
+
|
149
|
+
This way you will be implement it in your application:
|
150
|
+
|
151
|
+
#### protect views
|
152
|
+
|
153
|
+
```erb
|
154
|
+
<td><%= link_to 'Edit', edit_article_path(article) if current_user.dude.able_to_update_article?(article) %></td>
|
155
|
+
```
|
156
|
+
|
157
|
+
#### protect controllers
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
# app/controllers/articles_controller.rb
|
161
|
+
class ArticlesController < ApplicationController
|
162
|
+
# ...
|
163
|
+
|
164
|
+
def update
|
165
|
+
@article = Article.find_by(params[:id])
|
166
|
+
raise(DudePolicy::NotAuthorized) unless current_user.dude.able_to_update_article?(@article)
|
167
|
+
|
168
|
+
if @article.update(article_params)
|
169
|
+
redirect_to @article, notice: 'Article was successfully updated.'
|
170
|
+
else
|
171
|
+
render :edit
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
```
|
176
|
+
|
177
|
+
#### protect business logic
|
178
|
+
|
179
|
+
There are cases when you want to protect your business logic that is
|
180
|
+
beyond MVC level. For example:
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
# app/polcy/user_policy.rb
|
184
|
+
class UserPolicy < DudePolicy::BasePolicy
|
185
|
+
def able_to_see_user_full_name?(dude:)
|
186
|
+
return true if dude.admin?
|
187
|
+
return true if dude == resource
|
188
|
+
false
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
|
193
|
+
# app/helpers/application_helper.rb
|
194
|
+
def author_name(user)
|
195
|
+
return unless user
|
196
|
+
if current_user.dude.able_to_see_user_full_name?(user)
|
197
|
+
user.name
|
198
|
+
else
|
199
|
+
first_letter = user.name[0]
|
200
|
+
"#{first_letter}."
|
201
|
+
end
|
202
|
+
end
|
203
|
+
```
|
204
|
+
|
205
|
+
#### RSpec testing
|
206
|
+
|
207
|
+
|
208
|
+
##### policy test
|
209
|
+
|
210
|
+
You should be writing tests from perspective of current_user / current_account (the dude) and what roles they play.
|
211
|
+
|
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.
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
# spec/policy/article_policy_spec.rb
|
217
|
+
require 'rails_helper'
|
218
|
+
RSpec.describe ArticlePolicy do
|
219
|
+
let(:article) { create :article, author: author }
|
220
|
+
let(:author) { create :user }
|
221
|
+
|
222
|
+
context 'when nil user' do
|
223
|
+
let(:current_user) { nil }
|
224
|
+
|
225
|
+
it { expect(current_user.dude).not_to be_able_to_update_article(article) }
|
226
|
+
it { expect(current_user.dude).not_to be_able_to_delete_article(article) }
|
227
|
+
end
|
228
|
+
|
229
|
+
context 'when regular_user' do
|
230
|
+
let(:current_user) { create :user }
|
231
|
+
|
232
|
+
it { expect(current_user.dude).not_to be_able_to_update_article(article) }
|
233
|
+
it { expect(current_user.dude).not_to be_able_to_delete_article(article) }
|
234
|
+
|
235
|
+
context ' is author of article' do
|
236
|
+
let(:author) { current_user }
|
237
|
+
it { expect(current_user.dude).to be_able_to_update_article(article) }
|
238
|
+
it { expect(current_user.dude).to be_able_to_delete_article(article) }
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
context 'when admin' do
|
243
|
+
let(:current_user) { create :user, admin: true }
|
244
|
+
it { expect(current_user.dude).to be_able_to_update_article(article) }
|
245
|
+
it { expect(current_user.dude).not_to be_able_to_delete_article(article) }
|
246
|
+
|
247
|
+
context ' is author of article' do
|
248
|
+
let(:author) { current_user }
|
249
|
+
it { expect(current_user.dude).to be_able_to_update_article(article) }
|
250
|
+
it { expect(current_user.dude).to be_able_to_update_article(article) }
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
```
|
255
|
+
|
256
|
+
|
257
|
+
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
|
+
When it comes to policy tests this can lead to huge security disasters ([full explanation](https://blog.eq8.eu/assets/2019/unit-test.jpg))
|
259
|
+
|
260
|
+
##### request test
|
261
|
+
|
262
|
+
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
|
264
|
+
that means `allow(current_user.dude).to receive(:able_to_update_article?).and_return(true)`
|
265
|
+
|
266
|
+
> note: simmilar approach we would apply if you write controller RSpec test
|
267
|
+
|
268
|
+
|
269
|
+
```ruby
|
270
|
+
require 'rails_helper'
|
271
|
+
RSpec.describe "Articles", type: :request do
|
272
|
+
let(:article) { create :article }
|
273
|
+
|
274
|
+
describe "put /articles/xxxx" do
|
275
|
+
def trigger_update
|
276
|
+
if current_user
|
277
|
+
# devise login
|
278
|
+
sign_in current_user
|
279
|
+
|
280
|
+
# policies are tested with `spec/policy/article_spec.rb so we can stub
|
281
|
+
allow(current_user.dude)
|
282
|
+
.to receive(:able_to_update_article?)
|
283
|
+
.with(article)
|
284
|
+
.and_return(authorized)
|
285
|
+
end
|
286
|
+
|
287
|
+
put article_path(article), params: {format: :html, article: { title: 'cat' }}
|
288
|
+
article.reload
|
289
|
+
end
|
290
|
+
|
291
|
+
context 'when not authenticated' do
|
292
|
+
let(:current_user) { nil }
|
293
|
+
|
294
|
+
it { expect { trigger_update }.not_to change { article.title } }
|
295
|
+
it do
|
296
|
+
trigger_update
|
297
|
+
expect(response.status).to eq 302 # redirect by update
|
298
|
+
follow_redirect!
|
299
|
+
expect(response.body).to include('You need to sign in or sign up before continuing.')
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
context 'when authenticated' do
|
304
|
+
let(:current_user) { create :user }
|
305
|
+
|
306
|
+
context 'when not authorized' do
|
307
|
+
let(:authorized) { false }
|
308
|
+
|
309
|
+
it { expect { trigger_update }.not_to change { article.title } }
|
310
|
+
it do
|
311
|
+
trigger_update
|
312
|
+
expect(response.status).to eq 302 # redirect to root_path by `authenticate!` method is ApplicationController
|
313
|
+
follow_redirect!
|
314
|
+
expect(response.body).to include('Sorry current user is not authorized to perform this action')
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
context 'when authorized' do
|
319
|
+
let(:authorized) { true }
|
320
|
+
|
321
|
+
it { expect { trigger_update }.to change { article.title }.from('interesting article').to('cat') }
|
322
|
+
it do
|
323
|
+
trigger_update
|
324
|
+
expect(response.status).to eq 302
|
325
|
+
follow_redirect!
|
326
|
+
expect(response.body).to include('Article was successfully updated.')
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
```
|
333
|
+
|
334
|
+
|
335
|
+
#### More examples
|
336
|
+
|
337
|
+
> For more examples pls check the [example app](https://github.com/equivalent/dude_policy_example1)
|
338
|
+
|
99
339
|
|
100
340
|
## Contributing
|
101
341
|
|
@@ -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.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tomas Valent
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-04-
|
11
|
+
date: 2020-04-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|