active_entry 1.0.1 → 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: 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