active_entry 1.2.4 → 2.0.0

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: 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