active_entry 1.2.4 → 2.0.0

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: ab6abb6cb8a8069912d414cd395c1b57ee8327152f60ff9808db387eae70d3e2
4
- data.tar.gz: 48cf2369782b109f88208be35553075d253d78bd45151e5ca1c9e634143f8433
3
+ metadata.gz: f0d907b6dc39fa89d8c98128341eb2bd0804328b623ab473e5f6e8b3c9b6db0b
4
+ data.tar.gz: 4c105a1fedb63bc5ed4415e58184bb3b879744bd6656e477952c07ad898f15c3
5
5
  SHA512:
6
- metadata.gz: f65b2eb50012c246c91c98df7ef238de33ff1469893e611a58eea569843d9ec45bfe79099afc1f94e2bfe898a20d9e70fdb0678b190fde9f63dcf5051590e248
7
- data.tar.gz: 3f2b4ba32b292375292fa3e7c9c040de2cbb8d812f206b6c27e52fe7eb9da0013a5de6ae49c1052467fa07e5a83f73a4fc450b426963fbe74e14ef04ce231edf
6
+ metadata.gz: 85ce65d93de8ec106d94c4e00b38d68ee80cfe3c51cb9700284d8ae1f25247ee8b7c4c0ccdb43e595a1e49176525c79117b2ffa5f54f3a144f7586ebc081f9f9
7
+ data.tar.gz: ff50acbb6a52138618186aa2ec9d17a28807fff5bd66237c426f26db700e969c81b1113abb3dea695ee6479f46a537459d0c8b337f75abb7bd483514b7500ddc
data/README.md CHANGED
@@ -11,7 +11,64 @@
11
11
  [![Maintainability](https://api.codeclimate.com/v1/badges/3db0f653be6bdfe0fdac/maintainability)](https://codeclimate.com/github/TFM-Agency/active_entry/maintainability)
12
12
  [![Documentation](https://img.shields.io/badge/docs-rdoc.info-blue.svg)](https://rubydoc.info/github/TFM-Agency/active_entry/main)
13
13
 
14
- Active Entry is a simple and secure authentication and authorization system for your Rails application, which lets you to authenticate and authorize directly in your controllers.
14
+ Active Entry is a secure way to check for authentication and authorization before an action is performed. It's currently only compatible with Rails. But in later versions will ActiveEntry be Framework independent.
15
+
16
+ Active Entry works like many other Authorization Systems like [Pundit](https://github.com/varvet/pundit) or [Action Policy](https://github.com/palkan/action_policy) with **Policies**. However in Active Entry it's all about the method calling the auth mechanism. For every method that needs authentication or authorization, a decision maker method counterpart has to be created in the policy of the class.
17
+
18
+ ## Example
19
+
20
+ Let's say we have an Users controller in our application:
21
+
22
+ ```ruby
23
+ # app/controllers/users_controller.rb
24
+ class UsersController < ApplicationController
25
+ include ActiveEntry::ControllerConcern # Glue for the controller and Active Entry
26
+
27
+ def index
28
+ pass! # The auth happens here
29
+ load_users
30
+ end
31
+ end
32
+ ```
33
+
34
+ We have to create the UsersPolicy in order for Active Entry to know who is authenticated and authorized and who not.
35
+
36
+ ```ruby
37
+ # app/policies/users_policy.rb
38
+ module UsersPolicy
39
+ class Authentication < ActiveEntry::Base::Authentication
40
+ def index?
41
+ Current.user_signed_in? # Only signed in users are considered to be authenticated.
42
+ end
43
+ end
44
+
45
+ class Authorization < ActiveEntry::Base::Authorization
46
+ def index?
47
+ Current.user.admin? # Only admins are authorized to perform this action
48
+ end
49
+ end
50
+ end
51
+ ```
52
+
53
+ Now every time somebody calls the `users#index` endpoint, he or she has to be signed in and an admin. Otherwise `ActiveEntry::NotAuthenticatedError` or `ActiveEntry::NotAuthorizedError` are raised.
54
+ You can catch them easily in your controller by using Rails' `rescue_from`.
55
+
56
+ ```ruby
57
+ class ApplicationController < ActionController::Base
58
+ rescue_from ActiveEntry::NotAuthenticatedError, with: :not_authenticated
59
+ rescue_from ActiveEntry::NotAuthorizedError, with: :not_authorized
60
+
61
+ def not_authenticated
62
+ flash[:danger] = "Not authenticated. Please sign in."
63
+ redirect_to sign_in_path
64
+ end
65
+
66
+ def not_authorized
67
+ flash[:danger] = "Not authorized."
68
+ redirect_to root_path
69
+ end
70
+ end
71
+ ```
15
72
 
16
73
  ## Installation
17
74
  Add this line to your application's Gemfile:
@@ -20,72 +77,119 @@ Add this line to your application's Gemfile:
20
77
  gem 'active_entry'
21
78
  ```
22
79
 
23
- And then execute:
80
+ Or install it without bundler:
24
81
  ```bash
82
+ $ gem install active_entry
83
+ ```
84
+
85
+ Run Bundle:
86
+ ```shell
25
87
  $ bundle
26
88
  ```
27
89
 
28
- Or install it yourself as:
29
- ```bash
30
- $ gem install active_entry
90
+ And then install Active Entry:
91
+ ```shell
92
+ $ rails g active_entry:install
31
93
  ```
32
94
 
95
+ This will generate `app/policies/application_policy.rb`.
96
+
33
97
  ## Usage
34
- With Active Entry authentication and authorization is done in your Rails controllers. To enable authentication and authorization in one of your controllers, just add a before action for `authenticate!` and `authorize!` and the user has to authenticate and authorize on every call.
98
+ Active Entry works with Policies. You can generate policies the following way:
99
+
100
+ Let's consider the example from above.
101
+ We have an UsersController and we want a policy for that:
102
+
103
+ ```shell
104
+ $ rails g policy Users
105
+ ```
106
+
107
+ This generates a policy called `UsersPolicy` and is located in `app/policies/users_policy.rb`.
108
+
109
+ The above generator call would generate something like this, but with a few comments to help you get started:
110
+
111
+ ```ruby
112
+ module UsersPolicy
113
+ class Authentication < ActiveEntry::Base::Authentication
114
+ end
115
+
116
+ class Authorization < ActiveEntry::Base::Authorization
117
+ end
118
+ end
119
+ ```
35
120
 
36
121
  ### Verify authentication and authorization
37
- You probably want to control authentication and authorization for every controller action you have in your app. As a safeguard to ensure, that auth is performed in every controller and the call for auth is not forgotten in development, add the `#verify_authentication!` and `#verify_authorization` as after action callbacks to your `ApplicationController`.
122
+ You probably want to control authentication and authorization for every controller action you have in your app. As a safeguard to ensure, that auth is performed in every request and the auth call is not forgotten in development, add the `verify_authentication!` and `verify_authorization!` to your `ApplicationController`:
38
123
 
39
124
  ```ruby
40
125
  class ApplicationController < ActionController::Base
41
- before_action :verify_authentication!, :verify_authorization!
126
+ verify_authentication!
127
+ verify_authorization!
42
128
  # ...
43
129
  end
44
130
  ```
45
- This ensures, that you call `authenticate!` and/or `authorize!` in all your controllers and raises an `ActiveEntry::AuthenticationNotPerformedError` / `ActiveEntry::AuthorizationNotPerformedError` if not.
131
+ This ensures, that you perform auth in all your controllers and raises errors if not.
46
132
 
47
133
  ### Perform authentication and authorization
48
- in order to do the actual authentication and authorization, you have to add `authenticate!` and `authorize!` as before action callback in your controllers.
134
+ in order to do the actual authentication and authorization, you have to use `authenticate!` and `authorize!` or `pass!` as in your actions.
49
135
 
50
136
  ```ruby
51
- class DashboardController < ApplicationController
52
- before_action :authenticate!, :authorize!
53
- # ...
137
+ class UsersController < ApplicationController
138
+ def authentication_only_action
139
+ authenticate!
140
+ end
141
+
142
+ def authorization_only_action
143
+ authorize!
144
+ end
145
+
146
+ def both_authentication_and_authorization_action
147
+ pass!
148
+ end
54
149
  end
55
150
  ```
56
151
 
57
- If you try to open a page, you will get an `ActiveEntry::AuthenticationDecisionMakerMissingError` or `ActiveEntry::AuthorizationDecisionMakerMissingError`. This means that you have to instruct Active Entry when a user is authenticated/authorized and when not.
58
- You can do this by defining the methods `authenticated?` and `authorized?` in your controller.
152
+ If you try to open a page, Active Entry will raise `ActiveEntry::DecisionMakerMethodNotDefinedError`. This means we have to define the decision makers in our policy.
59
153
 
60
154
  ```ruby
61
- class DashboardController < ApplicationController
62
- # Actions ...
63
-
64
- private
155
+ module UsersPolicy
156
+ class Authentication < ApplicationPolicy::Authentication
157
+ def authentication_only_action?
158
+ success # == true | Everybody is allowed
159
+ end
65
160
 
66
- def authenticated?
67
- return true if user_signed_in?
161
+ def both_authentication_and_authorization_action?
162
+ success
163
+ end
68
164
  end
69
165
 
70
- def authorized?
71
- return true if current_user.admin?
72
- end
166
+ class Authorization < ApplicationPolicy::Authorization
167
+ def authorization_only_action?
168
+ success
169
+ end
170
+
171
+ def both_authentication_and_authorization_action?
172
+ success
173
+ end
174
+ end
73
175
  end
74
176
  ```
75
177
 
76
- Active Entry expects boolean return values from `authenticated?` and `authorized?`. `true` signals successful authentication/authorization, everything else not.
178
+ Every decision maker ends with an `?`. The name has to be the same as the name of the controller action. So `index` is going to be `index?`.
77
179
 
78
- ### Rescuing from errors
180
+ In order for Active Entry to not raise an auth error, the decision makers have to return `true`. In our above example we used `success`, which simply returns `true`.
79
181
 
80
- If the user is signed in, he is authenticated and authorized if he is an admin, otherwise an `ActiveEntry::NotAuthenticatedError` or `ActiveEntry::NotAuthorizedError` will be raised.
81
- Now you just have to catch this error and react accordingly. Rails has the convenient `rescue_from` for that.
182
+ **Note:** It has to be an explicit `true` and not just a truthy value. A string or object return value would raise an auth error.
183
+
184
+ ### Rescuing from errors
185
+ Catch the errors in your controllers to redirect the user or show them a message.
82
186
 
83
187
  ```ruby
84
188
  class ApplicationController < ActionController::Base
85
189
  # ...
86
190
 
87
- rescue_from ActiveEntry::NotAuthenticatedError, with: :not_authenticated unless Rails.env.test?
88
- rescue_from ActiveEntry::NotAuthorizedError, with: :not_authorized unless Rails.env.test?
191
+ rescue_from ActiveEntry::NotAuthenticatedError, with: :not_authenticated
192
+ rescue_from ActiveEntry::NotAuthorizedError, with: :not_authorized
89
193
 
90
194
  private
91
195
 
@@ -103,86 +207,70 @@ end
103
207
 
104
208
  In this example above, the user will be redirected with a flash message. But you can do whatever you want. For example logging.
105
209
 
106
- ### Scoped decision makers
107
-
108
- Instead of putting all authentication/authorization logic into `authenticated?` and `authorized?` you can create scoped decision makers:
210
+ ### Authenticate/authorize outside the action
211
+ You can authenticate and authorize outside the action:
109
212
 
110
213
  ```ruby
111
- class DashboardController < ApplicationController
112
- before_action :authenticate!, :authorize!
113
-
114
- def index_authenticated?
115
- # Do your authentication for the index action only
116
- end
117
- def index_authorized?
118
- # Do your authorization for the index action only
119
- end
120
- def index
121
- # Actual action
122
- end
214
+ class UsersController < ApplicationController
215
+ authenticate_now!
216
+ authorize_now!
217
+ # pass_now! # Does both, authentication and authorization
123
218
  end
124
219
  ```
125
220
 
126
- This puts authentication/authorization logic a lot closer to the actual action that is performed and you don't get lost in endlessly long `authenticated?` or `authorized?` decision maker methods.
127
-
128
- **Note:** The scoped authentication/authorization decision maker methods take precendence over the general ones. That means if you have an `index_authenticated?` for your index action defined, the general `authenticated?` gets ignored.
129
-
130
- ### Controller helper methods
131
-
132
- Active Entry also has a few helper methods which help you to distinguish between controller actions. You can check if a specific action got called, by adding `_action?` to the action name in your `authenticated?` or `authorized?`.
133
- For an action `show` this would be `show_action?`.
134
-
135
- **Note:** A `NoMethodError` gets raised if you try to call `_action?` if the actual action hasn't been implemented. For example `missing_implementation_action?` raises an error as long as `#missing_implementation` hasn't been implemented as action.
136
-
137
- The are some more helpers that check for more than one RESTful action:
221
+ Access control on class level will ensure that every action performs it.
138
222
 
139
- * `read_action?` - If the called action just read. Actions: `index`, `show`
140
- * `write_action?` - If the called action writes something. Actions: `new`, `create`, `edit`, `update`, `destroy`
141
- * `change_action?` - If something will be updated or destroyed. Actions: `edit`, `update`, `destroy`
142
- * `create_action?` - If something will be created. Actions: `new`, `create`
143
- * `update_action?` - If something will be updated. Actions: `edit`, `update`
144
- * `destroy_action?` - If something will be destroyed. Action: `destroy`
145
- * `delete_action?` - Alias for `destroy_action?`. Action: `destroy`
146
- * `collection_action?` - If the called action is a collection action. Actions: `index`, `new`, `create`
147
- * `member_action?` - Everything that is not a collection action. Including non-RESTful actions.
223
+ **Note:** Don't use the class methods if the controller is inherited in other controllers. Best, don't use them at all and use the methods in the actions conciously.
148
224
 
149
- So you can for example do:
225
+ ## Variables
226
+ You can pass variables to the decision maker.
150
227
 
151
228
  ```ruby
152
- class ApplicationController < ActionController::Base
153
- # ...
154
-
229
+ class UsersController < ApplicationController
155
230
  def show
231
+ @user = User.find params[:id]
232
+ pass! user: @user
156
233
  end
234
+ end
235
+ ```
157
236
 
158
- def custom
159
- end
160
-
161
- private
162
-
163
- def authorized?
164
- return true if read_action? # Everybody is authorized to call read actions
237
+ You can now access the user object as instance variable in your decision maker.
165
238
 
166
- if write_action?
167
- return true if admin_signed_in? # Just admins are allowed to call write actions
239
+ ```ruby
240
+ module Users
241
+ class Authentication < ApplicationPolicy::Authentication
242
+ def show?
243
+ @user # == <User:Instance>
168
244
  end
245
+ end
169
246
 
170
- if custom_action? # For custom/non-RESTful actions
171
- return true
247
+ class Authorization < ApplicationPolicy::Authorization
248
+ def show?
249
+ @user # == <User:Instance>
172
250
  end
173
251
  end
174
252
  end
175
253
  ```
176
254
 
177
- This is pretty much everything you have to do for basic authentication or authorization!
178
-
179
- ## Pass a custom error hash
180
- You can pass an error hash to the exception and use this in your rescue method:
255
+ ## Custom error data
256
+ If you write something into `@error` in our decision maker, you can access it in your rescue methods in the controller:
181
257
 
182
258
  ```ruby
259
+ module UsersPolicy
260
+ class Authentication < ApplicationPolicy::Authentication
261
+ def show?
262
+ @error = { code: 100 }
263
+ end
264
+ end
265
+
266
+ class Authorization < ApplicationPolicy::Authorization
267
+ def show?
268
+ @error = { code: 100 }
269
+ end
270
+ end
271
+ end
272
+
183
273
  class ApplicationController < ActionController::Base
184
- before_action :authenticate!, :authorize!
185
-
186
274
  # ...
187
275
 
188
276
  rescue_from ActiveEntry::NotAuthenticatedError, with: :not_authenticated
@@ -190,88 +278,134 @@ class ApplicationController < ActionController::Base
190
278
 
191
279
  private
192
280
 
193
- def not_authenticated(exception)
281
+ def not_authenticated exception
194
282
  flash[:danger] = "You are not authenticated! Code: #{exception.error[:code]}"
195
283
  redirect_to root_path
196
284
  end
197
285
 
198
- def not_authorized(exception)
286
+ def not_authorized exception
199
287
  flash[:danger] = "You are not authorized to call this action! Code: #{exception.error[:code]}"
200
288
  redirect_to root_path
201
289
  end
290
+ end
291
+ ```
202
292
 
203
- def authenticated?(error)
204
- error[:code] = "ERROR"
205
-
206
- return true if user_signed_in?
207
- end
208
-
209
- def authorized?(error)
210
- error[:code] = "ERROR"
293
+ But you can pass in whatever you want into your error hash.
211
294
 
212
- return true if read_action? # Everybody is authorized to call read actions
295
+ ## Testing
296
+ You can easily test your policies in RSpec. Let's start with the generator:
213
297
 
214
- if write_action?
215
- return true if admin_signed_in? # Just admins are allowed to call write actions
216
- end
217
- end
218
- end
298
+ ```shell
299
+ $ rails g rspec:policy Users
219
300
  ```
220
- ## Testing authentication and authorization
221
- If you check for the Rails environment with `unless Rails.env.test?` in your `rescue_from` statement you can easily test your authentication and authorization in your tests.
301
+
302
+ This will generate a spec for the `UsersPolicy` located in `spec/policies/users_policy_spec.rb`
222
303
 
223
304
  ```ruby
224
- class ApplicationController < ActionController::Base
225
- # ...
226
- rescue_from ActiveEntry::NotAuthenticatedError, with: :not_authenticated unless Rails.env.test?
227
- rescue_from ActiveEntry::NotAuthorizedError, with: :not_authorized unless Rails.env.test?
228
- # ...
305
+ require "rails_helper"
306
+
307
+ RSpec.describe UsersPolicy, type: :policy do
308
+ pending "add some examples to (or delete) #{__FILE__}"
229
309
  end
230
310
  ```
231
311
 
232
- Now you can catch `ActiveEntry::NotAuthenticatedError` / `ActiveEntry::NotAuthorizedError` in your test site like this:
312
+ Now you can easily test every decision maker with the `be_authenticated_for` and `be_authorized_for` matchers.
233
313
 
234
314
  ```ruby
235
315
  require "rails_helper"
236
316
 
237
- RSpec.describe "Users", type: :request do
238
- describe "Authentication" do
239
- context "#index" do
240
- context "authenticated" do
241
- it "as signed in user" do
242
- sign_in_as user
243
- expect{ get users_path }.to_not raise_error ActiveEntry::NotAuthenticatedError
244
- end
245
- end
246
-
247
- context "not authenticated" do
248
- it "as stranger" do
249
- expect{ get users_path }.to raise_error ActiveEntry::NotAuthenticatedError
250
- end
251
- end
317
+ RSpec.describe UsersPolicy, type: :policy do
318
+ describe UsersPolicy::Authentication do
319
+ subject { UsersPolicy::Authentication }
320
+
321
+ context "anonymous" do
322
+ it { is_expected.to_not be_authenticated_for :index }
323
+ it { is_expected.to be_authenticated_for :new }
324
+ it { is_expected.to be_authenticated_for :create }
325
+ it { is_expected.to_not be_authenticated_for :edit }
326
+ it { is_expected.to_not be_authenticated_for :update }
327
+ it { is_expected.to_not be_authenticated_for :destroy }
328
+ it { is_expected.to_not be_authenticated_for :restore }
329
+ end
330
+
331
+ context "signed in" do
332
+ before { Current.user = build :user }
333
+
334
+ it { is_expected.to be_authenticated_for :index }
335
+ it { is_expected.to be_authenticated_for :new }
336
+ it { is_expected.to be_authenticated_for :create }
337
+ it { is_expected.to be_authenticated_for :edit }
338
+ it { is_expected.to be_authenticated_for :update }
339
+ it { is_expected.to be_authenticated_for :destroy }
340
+ it { is_expected.to be_authenticated_for :restore }
252
341
  end
253
342
  end
254
343
 
255
- describe "Authorization" do
256
- context "#index" do
257
- context "authorized" do
258
- it "as admin" do
259
- sign_in_as admin
260
- expect{ get users_path }.to_not raise_error ActiveEntry::NotAuthorizedError
261
- end
262
- end
263
-
264
- context "not authenticated" do
265
- it "as non-admin" do
266
- sign_in_as user
267
- expect{ get users_path }.to raise_error ActiveEntry::NotAuthorizedError
268
- end
269
- end
344
+ describe UsersPolicy::Authorization do
345
+ subject { UsersPolicy::Authorization }
346
+
347
+ let(:user) { build :user }
348
+
349
+ context "anonymous" do
350
+ it { is_expected.to be_authorized_for :index }
351
+ it { is_expected.to be_authorized_for :new }
352
+ it { is_expected.to be_authorized_for :create }
353
+ it { is_expected.to be_authorized_for :show, user: user }
354
+ it { is_expected.to_not be_authorized_for :edit, user: user }
355
+ it { is_expected.to_not be_authorized_for :update, user: user }
356
+ it { is_expected.to_not be_authorized_for :destroy, user: user }
357
+ it { is_expected.to_not be_authorized_for :restore, user: user }
358
+ end
359
+
360
+ context "if @user is Current.user" do
361
+ before { Current.user = user }
362
+
363
+ it { is_expected.to be_authorized_for :show, user: user }
364
+ it { is_expected.to be_authorized_for :edit, user: user }
365
+ it { is_expected.to be_authorized_for :update, user: user }
366
+ it { is_expected.to be_authorized_for :destroy, user: user }
367
+ it { is_expected.to be_authorized_for :restore, user: user }
368
+ end
369
+
370
+ context "if @user is not Current.user" do
371
+ before { Current.user = build :user }
372
+
373
+ it { is_expected.to be_authorized_for :show, user: user }
374
+ it { is_expected.to_not be_authorized_for :edit, user: user }
375
+ it { is_expected.to_not be_authorized_for :update, user: user }
376
+ it { is_expected.to_not be_authorized_for :destroy, user: user }
377
+ it { is_expected.to_not be_authorized_for :restore, user: user }
270
378
  end
271
379
  end
272
380
  end
273
381
  ```
274
382
 
383
+ ## Differences to Action Policy
384
+ [Action Policy](https://github.com/palkan/action_policy) is an awesome gem which works pretty similar to Active Entry. But there are some differences:
385
+
386
+ ### Action Policy expects a performing subject and a target object
387
+ ```ruby
388
+ class PostPolicy < ApplicationPolicy
389
+ def update?
390
+ # `user` is a performing subject,
391
+ # `record` is a target object (post we want to update)
392
+ user.admin? || (user.id == record.user_id)
393
+ end
394
+ end
395
+ ```
396
+
397
+ In Active Entry you can pass in anything you want into the decision maker, which is accessible as instance variables. See Variables.
398
+
399
+ One strategy is not better than the other. It's just our preference.
400
+
401
+ ### Policies in Action Policy are for Resources/Models
402
+ If you have a `Post` model, you have a `PostPolicy` in Action Policy. In Active Entry you create policies for controllers. So if you have a `PostsController`, you have a `PostsPolicy`.
403
+ We like to build access control logic around controller endpoints.
404
+
405
+ ### Action Policy performs only authorization
406
+ Active Entry does technically also not provide authentication mechanisms. It's just that you place your authentication logic in an authentication decision maker.
407
+ We like both authentication and authorization logic in the same place but seperated hence `UsersPolicy::Authentication` and `UsersPolicy::Authorization`.
408
+
275
409
  ## Contributing
276
410
  Create pull requests on Github and help us to improve this Gem. There are some guidelines to follow:
277
411