active_entry 1.0.1 → 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: 15c793e52a2f7f0b43f1752bb61303f2fae758c763264fe3e73561ca3c272b8b
4
- data.tar.gz: 49ba592be85bb9f3f1642d748e4cbc235b4f49461dca4483773fb1de85252028
3
+ metadata.gz: f0d907b6dc39fa89d8c98128341eb2bd0804328b623ab473e5f6e8b3c9b6db0b
4
+ data.tar.gz: 4c105a1fedb63bc5ed4415e58184bb3b879744bd6656e477952c07ad898f15c3
5
5
  SHA512:
6
- metadata.gz: 1e009ca5bbd3b9c2d4153f96cf4dd864dd83196c317c276a7d2a71bde273325c6a59f2340e28ca6cadc83c4430da0fefa1f07846ffdd0d9e42409c2b91eab73e
7
- data.tar.gz: 9e62985d0726b6323126b3929d7a3b9ad2d1d6d51e8488da8a6f7061b8c2d9efb94f284b260d69d344f0a8f140afa5b1428a4fb9c20d632a037542ca1d28da34
6
+ metadata.gz: 85ce65d93de8ec106d94c4e00b38d68ee80cfe3c51cb9700284d8ae1f25247ee8b7c4c0ccdb43e595a1e49176525c79117b2ffa5f54f3a144f7586ebc081f9f9
7
+ data.tar.gz: ff50acbb6a52138618186aa2ec9d17a28807fff5bd66237c426f26db700e969c81b1113abb3dea695ee6479f46a537459d0c8b337f75abb7bd483514b7500ddc
data/README.md CHANGED
@@ -1,8 +1,74 @@
1
- [<img src="active_entry_logo.png" alt="Active Entry Logo" width="250px"/>](https://github.com/TFM-Agency/active_entry)
1
+ <p align="center">
2
+ <a href="https://github.com/TFM-Agency/active_entry">
3
+ <img src="https://raw.githubusercontent.com/TFM-Agency/active_entry/main/active_entry_logo.svg" alt="Active Entry Logo" width="350px"/>
4
+ </a>
5
+ </p>
2
6
 
3
7
  # Active Entry - Simple and flexible authentication and authorization
8
+ [![Gem Version](https://badge.fury.io/rb/active_entry.svg)](https://badge.fury.io/rb/active_entry)
9
+ [![Ruby](https://github.com/TFM-Agency/active_entry/actions/workflows/ci-rspec.yml/badge.svg)](https://github.com/TFM-Agency/active_entry/actions/workflows/ci-rspec.yml)
10
+ ![Coverage](https://raw.githubusercontent.com/TFM-Agency/active_entry/main/coverage/coverage_badge_total.svg)
11
+ [![Maintainability](https://api.codeclimate.com/v1/badges/3db0f653be6bdfe0fdac/maintainability)](https://codeclimate.com/github/TFM-Agency/active_entry/maintainability)
12
+ [![Documentation](https://img.shields.io/badge/docs-rdoc.info-blue.svg)](https://rubydoc.info/github/TFM-Agency/active_entry/main)
4
13
 
5
- 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
+ ```
6
72
 
7
73
  ## Installation
8
74
  Add this line to your application's Gemfile:
@@ -11,50 +77,112 @@ Add this line to your application's Gemfile:
11
77
  gem 'active_entry'
12
78
  ```
13
79
 
14
- And then execute:
80
+ Or install it without bundler:
15
81
  ```bash
82
+ $ gem install active_entry
83
+ ```
84
+
85
+ Run Bundle:
86
+ ```shell
16
87
  $ bundle
17
88
  ```
18
89
 
19
- Or install it yourself as:
20
- ```bash
21
- $ gem install active_entry
90
+ And then install Active Entry:
91
+ ```shell
92
+ $ rails g active_entry:install
22
93
  ```
23
94
 
95
+ This will generate `app/policies/application_policy.rb`.
96
+
24
97
  ## Usage
25
- 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.
26
- You probably want to control authentication and authorization for every controller action you have in your app. To enable this, just add the before action to the `ApplicationController`.
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
+ ```
120
+
121
+ ### Verify authentication and authorization
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`:
27
123
 
28
124
  ```ruby
29
125
  class ApplicationController < ActionController::Base
30
- before_action :authenticate!, :authorize!
126
+ verify_authentication!
127
+ verify_authorization!
31
128
  # ...
32
129
  end
33
130
  ```
131
+ This ensures, that you perform auth in all your controllers and raises errors if not.
34
132
 
35
- If you try to open a page, you will get an `ActiveEntry::AuthenticationNotPerformedError` or `ActiveEntry::AuthorizationNotPerformedError`. This means that you have to instruct Active Entry when a user is authenticated/authorized and when not.
36
- You can do this by defining the methods `authenticated?` and `authorized?` in your controller.
133
+ ### Perform authentication and authorization
134
+ in order to do the actual authentication and authorization, you have to use `authenticate!` and `authorize!` or `pass!` as in your actions.
37
135
 
38
136
  ```ruby
39
- class DashboardController < ApplicationController
40
- # Actions ...
137
+ class UsersController < ApplicationController
138
+ def authentication_only_action
139
+ authenticate!
140
+ end
41
141
 
42
- private
142
+ def authorization_only_action
143
+ authorize!
144
+ end
43
145
 
44
- def authenticated?
45
- return true if user_signed_in?
146
+ def both_authentication_and_authorization_action
147
+ pass!
46
148
  end
149
+ end
150
+ ```
47
151
 
48
- def authorized?
49
- return true if current_user.admin?
50
- end
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.
153
+
154
+ ```ruby
155
+ module UsersPolicy
156
+ class Authentication < ApplicationPolicy::Authentication
157
+ def authentication_only_action?
158
+ success # == true | Everybody is allowed
159
+ end
160
+
161
+ def both_authentication_and_authorization_action?
162
+ success
163
+ end
164
+ end
165
+
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
51
175
  end
52
176
  ```
53
177
 
54
- 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?`.
55
179
 
56
- 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.
57
- Now you just have to catch this error and react accordingly. Rails has the convinient `rescue_from` for that.
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`.
181
+
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.
58
186
 
59
187
  ```ruby
60
188
  class ApplicationController < ActionController::Base
@@ -79,38 +207,70 @@ end
79
207
 
80
208
  In this example above, the user will be redirected with a flash message. But you can do whatever you want. For example logging.
81
209
 
82
- Active Entry also has a few helper methods which help you to distinguish between RESTful controller actions.
210
+ ### Authenticate/authorize outside the action
211
+ You can authenticate and authorize outside the action:
212
+
213
+ ```ruby
214
+ class UsersController < ApplicationController
215
+ authenticate_now!
216
+ authorize_now!
217
+ # pass_now! # Does both, authentication and authorization
218
+ end
219
+ ```
83
220
 
84
- The following methods are available:
221
+ Access control on class level will ensure that every action performs it.
85
222
 
86
- * `read_action?` - If the called action just read. Actions: `index`, `show`
87
- * `write_action?` - If the called action writes something. Actions: `new`, `create`, `edit`, `update`, `destroy`
88
- * `change_action?` - If something will be updated or destroyed. Actions: `edit`, `update`, `destroy`
89
- * `create_action?` - If something will be created. Actions: `new`, `create`
90
- * `update_action?` - If something will be updated. Actions: `edit`, `update`
91
- * `destroy_action?` - If something will be destroyed. Action: `destroy`
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.
92
224
 
93
- So you can for example do:
225
+ ## Variables
226
+ You can pass variables to the decision maker.
94
227
 
95
228
  ```ruby
96
- def authorized?
97
- return true if read_action? # Everybody is authorized to call read actions
98
-
99
- if write_action?
100
- return true if admin_signed_in? # Just admins are allowed to call write actions
229
+ class UsersController < ApplicationController
230
+ def show
231
+ @user = User.find params[:id]
232
+ pass! user: @user
101
233
  end
102
234
  end
103
235
  ```
104
236
 
105
- This is pretty much everything you have to do for basic authentication or authorization!
237
+ You can now access the user object as instance variable in your decision maker.
106
238
 
107
- ## Pass a custom error hash
108
- You can pass an error hash to the exception and use this in your rescue method:
239
+ ```ruby
240
+ module Users
241
+ class Authentication < ApplicationPolicy::Authentication
242
+ def show?
243
+ @user # == <User:Instance>
244
+ end
245
+ end
246
+
247
+ class Authorization < ApplicationPolicy::Authorization
248
+ def show?
249
+ @user # == <User:Instance>
250
+ end
251
+ end
252
+ end
253
+ ```
254
+
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:
109
257
 
110
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
+
111
273
  class ApplicationController < ActionController::Base
112
- before_action :authenticate!, :authorize!
113
-
114
274
  # ...
115
275
 
116
276
  rescue_from ActiveEntry::NotAuthenticatedError, with: :not_authenticated
@@ -118,59 +278,134 @@ class ApplicationController < ActionController::Base
118
278
 
119
279
  private
120
280
 
121
- def not_authenticated(exception)
281
+ def not_authenticated exception
122
282
  flash[:danger] = "You are not authenticated! Code: #{exception.error[:code]}"
123
283
  redirect_to root_path
124
284
  end
125
285
 
126
- def not_authorized(exception)
286
+ def not_authorized exception
127
287
  flash[:danger] = "You are not authorized to call this action! Code: #{exception.error[:code]}"
128
288
  redirect_to root_path
129
289
  end
290
+ end
291
+ ```
130
292
 
131
- def authenticated?(error)
132
- error[:code] = "ERROR"
293
+ But you can pass in whatever you want into your error hash.
133
294
 
134
- return true if user_signed_in?
135
- end
136
-
137
- def authorized?(error)
138
- error[:code] = "ERROR"
295
+ ## Testing
296
+ You can easily test your policies in RSpec. Let's start with the generator:
139
297
 
140
- return true if read_action? # Everybody is authorized to call read actions
298
+ ```shell
299
+ $ rails g rspec:policy Users
300
+ ```
141
301
 
142
- if write_action?
143
- return true if admin_signed_in? # Just admins are allowed to call write actions
144
- end
145
- end
302
+ This will generate a spec for the `UsersPolicy` located in `spec/policies/users_policy_spec.rb`
303
+
304
+ ```ruby
305
+ require "rails_helper"
306
+
307
+ RSpec.describe UsersPolicy, type: :policy do
308
+ pending "add some examples to (or delete) #{__FILE__}"
146
309
  end
147
310
  ```
148
311
 
149
- ## Known Issues
150
- The authentication/authorization is done in a before action. These Rails controller before callbacks are done in defined order. If you set an instance variable which is needed in the `authenticated?` or `authorized?` method, you have to call the before action after the other method again.
151
-
152
- For example if you set `@user` in your controller in the `set_user` before action and you want to use the variable in `authorized?` action, you have to add the `authenticate!` or `authorize!` method after the `set_user` again, otherwise `@user` won't be available in `authenticate!` or `authorized?` yet.
312
+ Now you can easily test every decision maker with the `be_authenticated_for` and `be_authorized_for` matchers.
153
313
 
154
314
  ```ruby
155
- class UsersController < ApplicationController
156
- before_action :set_user
157
- before_action :authenticate!, :authorize!
315
+ require "rails_helper"
316
+
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
158
330
 
159
- def show
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 }
341
+ end
160
342
  end
161
343
 
162
- private
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 }
163
372
 
164
- def authenticated?
165
- return true if user_signed_in?
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 }
378
+ end
166
379
  end
380
+ end
381
+ ```
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:
167
385
 
168
- def authorized?
169
- return true if current_user == @user
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)
170
393
  end
171
394
  end
172
395
  ```
173
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
+
174
409
  ## Contributing
175
410
  Create pull requests on Github and help us to improve this Gem. There are some guidelines to follow:
176
411
 
@@ -179,4 +414,4 @@ Create pull requests on Github and help us to improve this Gem. There are some g
179
414
  * Document methods that aren't self-explaining (we are using [YARD](http://yardoc.org/))
180
415
 
181
416
  ## License
182
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
417
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,57 @@
1
+ require "active_support/concern"
2
+
3
+ module ActiveEntry
4
+ module ControllerConcern
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ # Methods .authenticate_now!, .authorize_now!, and .pass_now!
9
+ [:authenticate, :authorize, :pass].each do |method_name|
10
+ define_method "#{method_name}_now!" do
11
+ before_action do
12
+ args = {}
13
+ instance_variables.collect{ |v| v.to_s.remove("@").to_sym }.each do |name|
14
+ value = instance_variable_get ["@", name].join
15
+ next if value.nil?
16
+ args[name] = value
17
+ end
18
+ send "#{method_name}!", **args
19
+ end
20
+ end
21
+ end
22
+
23
+ def verify_authentication!
24
+ after_action do
25
+ raise AuthenticationNotPerformedError.new(self.class, action_name) unless @__authentication_done
26
+ end
27
+ end
28
+
29
+ def verify_authorization!
30
+ after_action do
31
+ raise AuthorizationNotPerformedError.new(self.class, action_name) unless @__authorization_done
32
+ end
33
+ end
34
+ end
35
+
36
+ def pass! **args
37
+ authenticate! **args
38
+ authorize! **args
39
+ end
40
+
41
+ def authenticate! **args
42
+ policy_class::Authentication.pass! action_name, **args unless @__authentication_done
43
+ @__authentication_done = true
44
+ end
45
+
46
+ def authorize! **args
47
+ policy_class::Authorization.pass! action_name, **args unless @__authorization_done
48
+ @__authorization_done = true
49
+ end
50
+
51
+ private
52
+
53
+ def policy_class
54
+ PolicyFinder.policy_for self.class.name.remove("Controller")
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,20 @@
1
+ module ActiveEntry
2
+ module ViewHelper
3
+ def authorized_for? controller_name, action, **args
4
+ controller_name = controller_name.to_s.camelize.remove "Controller"
5
+ policy = ActiveEntry::PolicyFinder.policy_for controller_name
6
+ policy::Authorization.pass? action, **args
7
+ end
8
+
9
+ def link_to_if_authorized name = nil, options = nil, html_options = nil, &block
10
+ url = url_for options
11
+ method = options&.is_a?(Hash) && options[:method] ? options[:method].to_s.upcase : "GET"
12
+
13
+ recognized_path = Rails.application.routes.recognize_path(url, method: method)
14
+
15
+ authorized = authorized_for? recognized_path[:controller], recognized_path[:action]
16
+
17
+ link_to name, options, html_options, &block if authorized
18
+ end
19
+ end
20
+ end
data/lib/active_entry.rb CHANGED
@@ -1,54 +1,13 @@
1
1
  require "active_entry/version"
2
+ require "active_entry/generators"
2
3
  require "active_entry/errors"
3
- require "active_entry/controller_methods"
4
- require "active_entry/railtie" if defined? Rails::Railtie
4
+ require "active_entry/base"
5
+ require "active_entry/policy_finder"
5
6
 
6
- module ActiveEntry
7
- # Authenticates the user
8
- def authenticate!
9
- # Raise an error if the #authenticate? action isn't defined.
10
- #
11
- # This ensures that you actually do authentication in your controller.
12
- raise ActiveEntry::AuthenticationNotPerformedError unless defined?(authenticated?)
13
-
14
- error = {}
15
- is_authenticated = nil
16
-
17
- if method(:authenticated?).arity > 0
18
- is_authenticated = authenticated?(error)
19
- else
20
- is_authenticated = authenticated?
21
- end
22
-
23
- # If the authenticated? method returns not true
24
- # it raises the ActiveEntry::NotAuthenticatedError.
25
- #
26
- # Use the .rescue_from method from ActionController::Base
27
- # to catch the exception and show the user a proper error message.
28
- raise ActiveEntry::NotAuthenticatedError.new(error) unless is_authenticated == true
29
- end
30
-
31
- # Authorizes the user.
32
- def authorize!
33
- # Raise an error if the #authorize? action isn't defined.
34
- #
35
- # This ensures that you actually do authorization in your controller.
36
- raise ActiveEntry::AuthorizationNotPerformedError unless defined?(authorized?)
37
-
38
- error = {}
39
- is_authorized = nil
7
+ require_relative "../app/controllers/concerns/active_entry/concern" if defined?(ActionController::Base)
8
+ require 'active_entry/railtie' if defined?(Rails)
40
9
 
41
- if method(:authorized?).arity > 0
42
- is_authorized = authorized?(error)
43
- else
44
- is_authorized = authorized?
45
- end
10
+ require "active_support/inflector"
46
11
 
47
- # If the authorized? method does not return true
48
- # it raises the ActiveEntry::NotAuthorizedError
49
- #
50
- # Use the .rescue_from method from ActionController::Base
51
- # to catch the exception and show the user a proper error message.
52
- raise ActiveEntry::NotAuthorizedError.new(error) unless is_authorized == true
53
- end
12
+ module ActiveEntry
54
13
  end
@@ -0,0 +1,66 @@
1
+ module ActiveEntry
2
+ class Base
3
+ class Authentication < Base
4
+ AUTH_ERROR = NotAuthenticatedError
5
+
6
+ def self.pass! method_name, **args
7
+ new(method_name, **args).pass!
8
+ end
9
+
10
+ def self.pass? method_name, **args
11
+ new(method_name, **args).pass?
12
+ end
13
+ end
14
+
15
+ class Authorization < Base
16
+ AUTH_ERROR = NotAuthorizedError
17
+
18
+ def self.pass! method_name, **args
19
+ new(method_name, **args).pass!
20
+ end
21
+
22
+ def self.pass? method_name, **args
23
+ new(method_name, **args).pass?
24
+ end
25
+ end
26
+
27
+ def initialize method_name, **args
28
+ @_method_name_to_entrify = method_name
29
+ @_args = args
30
+ @_args.each { |name, value| instance_variable_set ["@", name].join, value }
31
+ end
32
+
33
+ class << self
34
+ def pass! method_name, **args
35
+ Authentication.pass! method_name, **args
36
+ Authorization.pass! method_name, **args
37
+ end
38
+
39
+ def pass? method_name, **args
40
+ Authentication.pass? method_name, **args
41
+ Authorization.pass? method_name, **args
42
+ end
43
+ end
44
+
45
+
46
+ def pass!
47
+ pass? or raise self.class::AUTH_ERROR.new(@error, @_method_name_to_entrify, @_args)
48
+ end
49
+
50
+ def pass?
51
+ decision_maker_method.call == true
52
+ end
53
+
54
+ def success
55
+ true
56
+ end
57
+
58
+ private
59
+
60
+ def decision_maker_method
61
+ decision_maker_method_name = [@_method_name_to_entrify, "?"].join
62
+ raise DecisionMakerMethodNotDefinedError.new(self.class, decision_maker_method_name) unless respond_to?(decision_maker_method_name)
63
+ method decision_maker_method_name
64
+ end
65
+ end
66
+ end
@@ -1,66 +1,65 @@
1
- # @author Tobias Feistmantl
2
1
  module ActiveEntry
3
- # Generic authorization error.
4
- # Other, more specific, errors inherit from this one.
5
- #
6
- # @raise [AuthorizationError]
7
- # if something generic is happening.
8
- class AuthorizationError < StandardError
2
+ class Error < StandardError
9
3
  end
10
4
 
11
- # Error for controllers in which authorization isn't handled.
12
- #
13
- # @raise [AuthorizationNotPerformedError]
14
- # if the #authorized? method isn't defined
15
- # in the controller class.
16
- class AuthorizationNotPerformedError < AuthorizationError
5
+ class AuthError < Error
6
+ attr_reader :error, :method, :arguments
7
+
8
+ def initialize error, method, arguments
9
+ @error = error
10
+ @method = method
11
+ @arguments = arguments
12
+ @message = "Not authenticated/authorized for method ##{@method}"
13
+
14
+ super @message
15
+ end
17
16
  end
18
17
 
19
- # Error if user unauthorized.
20
- #
21
- # @raise [NotAuthorizedError]
22
- # if authorized? isn't returning true.
23
- #
24
- # @note
25
- # Should always be called at the end
26
- # of the #authorize! method.
27
- class NotAuthorizedError < AuthorizationError
28
- attr_reader :error
18
+ class NotAuthenticatedError < AuthError
19
+ end
29
20
 
30
- def initialize(error={})
31
- @error = error
21
+ class NotAuthorizedError < AuthError
22
+ end
23
+
24
+ class NotPerformedError < Error
25
+ attr_reader :class_name, :method
26
+
27
+ def initialize class_name, method
28
+ @class_name = class_name
29
+ @method = method
30
+ @message = "Auth not performed for #{@class_name}##{@method}."
31
+
32
+ super @message
32
33
  end
33
34
  end
34
35
 
36
+ class AuthenticationNotPerformedError < NotPerformedError
37
+ end
35
38
 
36
- # Base class for authentication errors.
37
- #
38
- # @raise [AuthenticationError]
39
- # if something generic happens.
40
- class AuthenticationError < StandardError
39
+ class AuthorizationNotPerformedError < NotPerformedError
41
40
  end
42
41
 
43
- # Error for controllers in which authentication isn't handled.
44
- #
45
- # @raise [AuthenticationNotPerformedError]
46
- # if the #authenticated? method isn't defined
47
- # in the controller class.
48
- class AuthenticationNotPerformedError < AuthenticationError
42
+ class NotDefinedError < Error
43
+ attr_reader :policy_name, :class_name
44
+
45
+ def initialize policy_name, class_name
46
+ @policy_name = policy_name
47
+ @class_name = class_name
48
+ @message = "Policy #{policy_name} for class #{@class_name} not defined."
49
+
50
+ super @message
51
+ end
49
52
  end
50
53
 
51
- # Error if user not authenticated
52
- #
53
- # @raise [NotAuthenticatedError]
54
- # if authenticated? isn't returning true.
55
- #
56
- # @note
57
- # Should always be called at the end
58
- # of the #authenticate! method.
59
- class NotAuthenticatedError < AuthenticationError
60
- attr_reader :error
54
+ class DecisionMakerMethodNotDefinedError < Error
55
+ attr_reader :policy_name, :decision_maker_method_name
61
56
 
62
- def initialize(error={})
63
- @error = error
57
+ def initialize policy_name, decision_maker_method_name
58
+ @policy_name = policy_name
59
+ @decision_maker_method_name = decision_maker_method_name
60
+ @message = "Decision maker #{policy_name}##{decision_maker_method_name} is not defined."
61
+
62
+ super @message
64
63
  end
65
64
  end
66
65
  end
@@ -0,0 +1,4 @@
1
+ module ActiveEntry
2
+ module Generators
3
+ end
4
+ end
@@ -0,0 +1,25 @@
1
+ module ActiveEntry
2
+ class PolicyFinder
3
+ attr_reader :class_name
4
+
5
+ def initialize class_name
6
+ @class_name = class_name
7
+ end
8
+
9
+ class << self
10
+ def policy_for class_name
11
+ new(class_name).policy
12
+ end
13
+ end
14
+
15
+ def policy
16
+ policy_class_name.safe_constantize or raise NotDefinedError.new(policy_class_name, @class_name)
17
+ end
18
+
19
+ private
20
+
21
+ def policy_class_name
22
+ [@class_name, "Policy"].join
23
+ end
24
+ end
25
+ end
@@ -1,9 +1,9 @@
1
+ require_relative '../../app/helpers/active_entry/view_helper'
2
+
1
3
  module ActiveEntry
2
- class Railtie < ::Rails::Railtie
3
- initializer 'active_entry.include_in_action_controller' do
4
- ActiveSupport.on_load :action_controller do
5
- ::ActionController::Base.include(ActiveEntry)
6
- end
4
+ class Railtie < Rails::Railtie
5
+ initializer "active_entry.view_helper" do
6
+ ActiveSupport.on_load(:action_view) { include ActiveEntry::ViewHelper }
7
7
  end
8
8
  end
9
- end
9
+ end
@@ -0,0 +1,56 @@
1
+ module ActiveEntry
2
+ module Rspec
3
+ module Matchers
4
+ extend ::RSpec::Matchers::DSL
5
+
6
+ matcher :be_authenticated_for do |action, **args|
7
+ match do |policy|
8
+ policy.pass? action, **args
9
+ end
10
+
11
+ description do
12
+ "be authenticated for #{action}"
13
+ end
14
+
15
+ failure_message do |policy|
16
+ "expected that #{policy} passes authentication for #{action}"
17
+ end
18
+ end
19
+
20
+ matcher :be_authorized_for do |action, **args|
21
+ match do |policy|
22
+ policy.pass? action, **args
23
+ end
24
+
25
+ description do
26
+ "be authorized for #{action}"
27
+ end
28
+
29
+ failure_message do |policy|
30
+ "expected that #{policy} passes authorization for #{action}"
31
+ end
32
+ end
33
+ end
34
+
35
+ module DSL
36
+ end
37
+
38
+ module PolicyExampleGroup
39
+ include ActiveEntry::Rspec::Matchers
40
+
41
+ def self.included(base)
42
+ base.metadata[:type] = :policy
43
+ base.extend ActiveEntry::Rspec::DSL
44
+ super
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ RSpec.configure do |config|
51
+ config.include(
52
+ ActiveEntry::Rspec::PolicyExampleGroup,
53
+ type: :policy,
54
+ file_path: %r{spec/policies}
55
+ )
56
+ end
@@ -1,3 +1,3 @@
1
1
  module ActiveEntry
2
- VERSION = '1.0.1'
2
+ VERSION = '2.0.0'
3
3
  end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Installs Active Entry. Generates an ApplicationPolicy.
3
+
4
+ Example:
5
+ bin/rails generate active_entry:install
6
+
7
+ This will create:
8
+ app/policies/application_policy.rb
@@ -0,0 +1,9 @@
1
+ module ActiveEntry
2
+ class InstallGenerator < Rails::Generators::Base
3
+ source_root File.expand_path('templates', __dir__)
4
+
5
+ def create_application_policy
6
+ template "application_policy.rb", File.join("app/policies/application_policy.rb"), skip: true
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ module ApplicationPolicy
2
+ class Authentication < ActiveEntry::Base::Authentication
3
+ end
4
+
5
+ class Authorization < ActiveEntry::Base::Authorization
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generates a new policy class for authentication/authorization.
3
+
4
+ Example:
5
+ bin/rails generate policy Users
6
+
7
+ This will create:
8
+ app/policies/users_policy.rb
@@ -0,0 +1,7 @@
1
+ class PolicyGenerator < Rails::Generators::NamedBase
2
+ source_root File.expand_path('templates', __dir__)
3
+
4
+ def create_policy
5
+ template "policy.rb", File.join("app/policies", class_path, "#{file_name}_policy.rb")
6
+ end
7
+ end
@@ -0,0 +1,53 @@
1
+ module <%= class_name %>Policy
2
+ class Authentication < ApplicationPolicy::Authentication
3
+ # It's all about decision makers. In your decision makers you tell
4
+ # Active Entry when and if somebody is authenticated/authorized.
5
+ #
6
+ # You can declare decision makers for any method you want.
7
+ # Just use the same name as your action and add a ? at the end.
8
+
9
+ # def index?
10
+ # end
11
+
12
+ # def new?
13
+ # end
14
+
15
+ # def create?
16
+ # end
17
+
18
+ # def show?
19
+ # end
20
+
21
+ # def edit?
22
+ # end
23
+
24
+ # def update?
25
+ # end
26
+
27
+ # def destroy?
28
+ # end
29
+ end
30
+
31
+ class Authorization < ApplicationPolicy::Authorization
32
+ # def index?
33
+ # end
34
+
35
+ # def new?
36
+ # end
37
+
38
+ # def create?
39
+ # end
40
+
41
+ # def show?
42
+ # end
43
+
44
+ # def edit?
45
+ # end
46
+
47
+ # def update?
48
+ # end
49
+
50
+ # def destroy?
51
+ # end
52
+ end
53
+ end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generates a new policy spec to test authentication/authorization.
3
+
4
+ Example:
5
+ bin/rails generate rspec:policy Users
6
+
7
+ This will create:
8
+ spec/policies/users_policy_spec.rb
@@ -0,0 +1,11 @@
1
+ module Rspec
2
+ module Generators
3
+ class PolicyGenerator < Rails::Generators::NamedBase
4
+ source_root File.expand_path("templates", __dir__)
5
+
6
+ def create_policy_spec
7
+ template "policy_spec.rb", File.join("spec/policies", class_path, "#{file_name}_policy_spec.rb")
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ require "<%= File.exists?('spec/rails_helper.rb') ? 'rails_helper' : 'spec_helper' %>"
2
+
3
+ RSpec.describe <%= class_name %>Policy, type: :policy do
4
+ pending "add some examples to (or delete) #{__FILE__}"
5
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_entry
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - TFM Agency GmbH
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-03-02 00:00:00.000000000 Z
12
+ date: 2021-04-02 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -67,8 +67,22 @@ dependencies:
67
67
  - - ">="
68
68
  - !ruby/object:Gem::Version
69
69
  version: '0'
70
- description: An easy and flexible access control system. No need for policies, abilities,
71
- etc. Do authentication and authorization directly in your controller.
70
+ - !ruby/object:Gem::Dependency
71
+ name: ffaker
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ description: An easy and flexible access control system. Authentication and authorization
85
+ before a method/action is executed.
72
86
  email:
73
87
  - hello@tfm.agency
74
88
  executables: []
@@ -78,11 +92,25 @@ files:
78
92
  - MIT-LICENSE
79
93
  - README.md
80
94
  - Rakefile
95
+ - app/controllers/concerns/active_entry/concern.rb
96
+ - app/helpers/active_entry/view_helper.rb
81
97
  - lib/active_entry.rb
82
- - lib/active_entry/controller_methods.rb
98
+ - lib/active_entry/base.rb
83
99
  - lib/active_entry/errors.rb
100
+ - lib/active_entry/generators.rb
101
+ - lib/active_entry/policy_finder.rb
84
102
  - lib/active_entry/railtie.rb
103
+ - lib/active_entry/rspec.rb
85
104
  - lib/active_entry/version.rb
105
+ - lib/generators/active_entry/install/USAGE
106
+ - lib/generators/active_entry/install/install_generator.rb
107
+ - lib/generators/active_entry/install/templates/application_policy.rb
108
+ - lib/generators/policy/USAGE
109
+ - lib/generators/policy/policy_generator.rb
110
+ - lib/generators/policy/templates/policy.rb
111
+ - lib/generators/rspec/USAGE
112
+ - lib/generators/rspec/policy_generator.rb
113
+ - lib/generators/rspec/templates/policy_spec.rb
86
114
  - lib/tasks/active_entry_tasks.rake
87
115
  homepage: https://github.com/TFM-Agency/active_entry
88
116
  licenses:
@@ -1,83 +0,0 @@
1
- # @author Tobias Feistmantl
2
- #
3
- # Helper methods for your controller
4
- # to identify RESTful actions.
5
- module ActiveEntry
6
- # @return [Boolean]
7
- # True if the called action
8
- # is a only-read action.
9
- def read_action?
10
- action_name == 'index' ||
11
- action_name == 'show'
12
- end
13
-
14
- # @return [Boolean]
15
- # True if the called action
16
- # is a write action.
17
- def write_action?
18
- action_name == 'new' ||
19
- action_name == 'create' ||
20
- action_name == 'edit' ||
21
- action_name == 'update' ||
22
- action_name == 'destroy'
23
- end
24
-
25
- # @return [Boolean]
26
- # True if the called action
27
- # is a change action.
28
- def change_action?
29
- action_name == 'edit' ||
30
- action_name == 'update' ||
31
- action_name == 'destroy'
32
- end
33
-
34
- # @return [Boolean]
35
- # True if the called action
36
- # is the index action.
37
- def index_action?
38
- action_name == 'index'
39
- end
40
-
41
- # @return [Boolean]
42
- # True if the called action
43
- # is the show action.
44
- def show_action?
45
- action_name == 'show'
46
- end
47
-
48
- # @note
49
- # Also true for the pseudo
50
- # update action `new`.
51
- #
52
- # @note
53
- # Only true for create methods
54
- # such as new and create.
55
- #
56
- # @return [Boolean]
57
- # True if the called action
58
- # is a create action.
59
- def create_action?
60
- action_name == 'new' ||
61
- action_name == 'create'
62
- end
63
-
64
- # @note
65
- # Also true for the pseudo
66
- # update action `edit`.
67
- #
68
- # @return [Boolean]
69
- # True if the called action
70
- # is a update action.
71
- def update_action?
72
- action_name == 'edit' ||
73
- action_name == 'update'
74
- end
75
-
76
- # @return [Boolean]
77
- # True if it's a destroy action.
78
- def destroy_action?
79
- action_name == 'destroy'
80
- end
81
-
82
- alias delete_action? destroy_action?
83
- end