active_entry 1.1.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e6ea0fd23af731840ed3e6f003c7b6beff6a14b2922bbb96444a5bfbeec58ae
4
- data.tar.gz: b4a47cede63aa58e6f1675ab704fce6b7a7e4c4b46fc791330c4e770158bc6db
3
+ metadata.gz: 68c5d6e98c46a35b4ef4e32f6987bd0c412326aad17db589e45dd671a6d3e1b7
4
+ data.tar.gz: 48d4b700fdff06bd94ab4fc2b9d34c73142b21a91641be20fd080287f11c16f4
5
5
  SHA512:
6
- metadata.gz: c1dba6e952921afc6260b4a7e54878f2db97235f5d5c531dc769a34daf8bb65682f8d377670265000697e8715495f9a152a2be25aa05211a5916109e0db18e69
7
- data.tar.gz: 5e813dd7d89ec9eb0514b470fcc3b1a7ffb256ea5d20f3f53e75bfc029a072dbff813a9413127cf83818e3d2d173f22240c7d91f0c3c2da335d069f0edd8b1e8
6
+ metadata.gz: 4efc966d6cc59cf94da405cdd583afb3e3954ed73dbb97b9c83876cd04ffe5965e7d67850b903318acbf9af342ea0933c837b65773bf74deb7c8664c0e58d3ff
7
+ data.tar.gz: ac48a14a4a6b0761b3d35516caa11254c8b637dda53498b6b603d16ba1b800e5d09e44af8383ea9141777907288381648b0b39db033f55db84899e1e671e084f
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,52 +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!
148
+ end
149
+ end
150
+ ```
151
+
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
46
164
  end
47
165
 
48
- def authorized?
49
- return true if current_user.admin?
50
- 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
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
- ### 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`.
57
181
 
58
- 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.
59
- 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.
60
186
 
61
187
  ```ruby
62
188
  class ApplicationController < ActionController::Base
@@ -81,84 +207,70 @@ end
81
207
 
82
208
  In this example above, the user will be redirected with a flash message. But you can do whatever you want. For example logging.
83
209
 
84
- ### Scoped decision makers
85
-
86
- 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:
87
212
 
88
213
  ```ruby
89
- class DashboardController < ApplicationController
90
- before_action :authenticate!, :authorize!
91
-
92
- def index_authenticated?
93
- # Do your authentication for the index action only
94
- end
95
- def index_authorized?
96
- # Do your authorization for the index action only
97
- end
98
- def index
99
- # Actual action
100
- end
214
+ class UsersController < ApplicationController
215
+ authenticate_now!
216
+ authorize_now!
217
+ # pass_now! # Does both, authentication and authorization
101
218
  end
102
219
  ```
103
220
 
104
- 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.
105
-
106
- **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.
107
-
108
- ### Controller helper methods
109
-
110
- 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?`.
111
- For an action `show` this would be `show_action?`.
221
+ Access control on class level will ensure that every action performs it.
112
222
 
113
- **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.
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.
114
224
 
115
- The are some more helpers that check for more than one RESTful action:
116
-
117
- * `read_action?` - If the called action just read. Actions: `index`, `show`
118
- * `write_action?` - If the called action writes something. Actions: `new`, `create`, `edit`, `update`, `destroy`
119
- * `change_action?` - If something will be updated or destroyed. Actions: `edit`, `update`, `destroy`
120
- * `create_action?` - If something will be created. Actions: `new`, `create`
121
- * `update_action?` - If something will be updated. Actions: `edit`, `update`
122
- * `destroy_action?` - If something will be destroyed. Action: `destroy`
123
- * `delete_action?` - Alias for `destroy_action?`. Action: `destroy`
124
-
125
- So you can for example do:
225
+ ## Variables
226
+ You can pass variables to the decision maker.
126
227
 
127
228
  ```ruby
128
- class ApplicationController < ActionController::Base
129
- # ...
130
-
229
+ class UsersController < ApplicationController
131
230
  def show
231
+ @user = User.find params[:id]
232
+ pass! user: @user
132
233
  end
234
+ end
235
+ ```
133
236
 
134
- def custom
135
- end
136
-
137
- private
138
-
139
- def authorized?
140
- 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.
141
238
 
142
- if write_action?
143
- 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>
144
244
  end
245
+ end
145
246
 
146
- if custom_action? # For custom/non-RESTful actions
147
- return true
247
+ class Authorization < ApplicationPolicy::Authorization
248
+ def show?
249
+ @user # == <User:Instance>
148
250
  end
149
251
  end
150
252
  end
151
253
  ```
152
254
 
153
- This is pretty much everything you have to do for basic authentication or authorization!
154
-
155
- ## Pass a custom error hash
156
- 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:
157
257
 
158
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
+
159
273
  class ApplicationController < ActionController::Base
160
- before_action :authenticate!, :authorize!
161
-
162
274
  # ...
163
275
 
164
276
  rescue_from ActiveEntry::NotAuthenticatedError, with: :not_authenticated
@@ -166,59 +278,134 @@ class ApplicationController < ActionController::Base
166
278
 
167
279
  private
168
280
 
169
- def not_authenticated(exception)
281
+ def not_authenticated exception
170
282
  flash[:danger] = "You are not authenticated! Code: #{exception.error[:code]}"
171
283
  redirect_to root_path
172
284
  end
173
285
 
174
- def not_authorized(exception)
286
+ def not_authorized exception
175
287
  flash[:danger] = "You are not authorized to call this action! Code: #{exception.error[:code]}"
176
288
  redirect_to root_path
177
289
  end
290
+ end
291
+ ```
178
292
 
179
- def authenticated?(error)
180
- error[:code] = "ERROR"
293
+ But you can pass in whatever you want into your error hash.
181
294
 
182
- return true if user_signed_in?
183
- end
184
-
185
- def authorized?(error)
186
- error[:code] = "ERROR"
295
+ ## Testing
296
+ You can easily test your policies in RSpec. Let's start with the generator:
187
297
 
188
- return true if read_action? # Everybody is authorized to call read actions
298
+ ```shell
299
+ $ rails g rspec:policy Users
300
+ ```
189
301
 
190
- if write_action?
191
- return true if admin_signed_in? # Just admins are allowed to call write actions
192
- end
193
- 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__}"
194
309
  end
195
310
  ```
196
311
 
197
- ## Known Issues
198
- 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.
199
-
200
- 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.
201
313
 
202
314
  ```ruby
203
- class UsersController < ApplicationController
204
- before_action :set_user
205
- 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
206
330
 
207
- 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
208
342
  end
209
343
 
210
- 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
211
359
 
212
- def authenticated?
213
- return true if user_signed_in?
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 }
378
+ end
214
379
  end
380
+ end
381
+ ```
215
382
 
216
- def authorized?
217
- return true if current_user == @user
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)
218
393
  end
219
394
  end
220
395
  ```
221
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
+
222
409
  ## Contributing
223
410
  Create pull requests on Github and help us to improve this Gem. There are some guidelines to follow:
224
411
 
@@ -227,4 +414,4 @@ Create pull requests on Github and help us to improve this Gem. There are some g
227
414
  * Document methods that aren't self-explaining (we are using [YARD](http://yardoc.org/))
228
415
 
229
416
  ## License
230
- 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, **args, &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], **args
16
+
17
+ link_to name, options, html_options, &block if authorized
18
+ end
19
+ end
20
+ end
data/lib/active_entry.rb CHANGED
@@ -1,76 +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
- general_decision_maker_method_name = :authenticated?
10
- scoped_decision_maker_method_name = [action_name, :authenticated?].join("_").to_sym
11
-
12
- general_decision_maker_defined = respond_to? general_decision_maker_method_name, true
13
- scoped_decision_maker_defined = respond_to? scoped_decision_maker_method_name, true
14
-
15
- # Check if a scoped decision maker method is defined and use it over
16
- # general decision maker method.
17
- decision_maker_to_use = scoped_decision_maker_defined ? scoped_decision_maker_method_name : general_decision_maker_method_name
18
-
19
- # Raise an error if the #authenticate? action isn't defined.
20
- #
21
- # This ensures that you actually do authentication in your controller.
22
- if !scoped_decision_maker_defined && !general_decision_maker_defined
23
- raise ActiveEntry::AuthenticationNotPerformedError
24
- end
25
-
26
- error = {}
27
-
28
- if method(decision_maker_to_use).arity > 0
29
- is_authenticated = send decision_maker_to_use, error
30
- else
31
- is_authenticated = send decision_maker_to_use
32
- end
33
-
34
- # If the authenticated? method returns not true
35
- # it raises the ActiveEntry::NotAuthenticatedError.
36
- #
37
- # Use the .rescue_from method from ActionController::Base
38
- # to catch the exception and show the user a proper error message.
39
- raise ActiveEntry::NotAuthenticatedError.new(error) unless is_authenticated == true
40
- end
41
-
42
- # Authorizes the user.
43
- def authorize!
44
- general_decision_maker_method_name = :authorized?
45
- scoped_decision_maker_method_name = [action_name, :authorized?].join("_").to_sym
7
+ require_relative "../app/controllers/concerns/active_entry/concern" if defined?(ActionController::Base)
8
+ require 'active_entry/railtie' if defined?(Rails)
46
9
 
47
- general_decision_maker_defined = respond_to? general_decision_maker_method_name, true
48
- scoped_decision_maker_defined = respond_to? scoped_decision_maker_method_name, true
10
+ require "active_support/inflector"
49
11
 
50
- # Check if a scoped decision maker method is defined and use it over
51
- # general decision maker method.
52
- decision_maker_to_use = scoped_decision_maker_defined ? scoped_decision_maker_method_name : general_decision_maker_method_name
53
-
54
- # Raise an error if the #authorize? action isn't defined.
55
- #
56
- # This ensures that you actually do authorization in your controller.
57
- if !scoped_decision_maker_defined && !general_decision_maker_defined
58
- raise ActiveEntry::AuthorizationNotPerformedError
59
- end
60
-
61
- error = {}
62
-
63
- if method(decision_maker_to_use).arity > 0
64
- is_authorized = send(decision_maker_to_use, error)
65
- else
66
- is_authorized = send(decision_maker_to_use)
67
- end
68
-
69
- # If the authorized? method does not return true
70
- # it raises the ActiveEntry::NotAuthorizedError
71
- #
72
- # Use the .rescue_from method from ActionController::Base
73
- # to catch the exception and show the user a proper error message.
74
- raise ActiveEntry::NotAuthorizedError.new(error) unless is_authorized == true
75
- end
12
+ module ActiveEntry
76
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.1.0'
2
+ VERSION = '2.0.1'
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.1.0
4
+ version: 2.0.1
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-03 00:00:00.000000000 Z
12
+ date: 2021-04-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -81,8 +81,8 @@ dependencies:
81
81
  - - ">="
82
82
  - !ruby/object:Gem::Version
83
83
  version: '0'
84
- description: An easy and flexible access control system. No need for policies, abilities,
85
- etc. Do authentication and authorization directly in your controller.
84
+ description: An easy and flexible access control system. Authentication and authorization
85
+ before a method/action is executed.
86
86
  email:
87
87
  - hello@tfm.agency
88
88
  executables: []
@@ -92,11 +92,25 @@ files:
92
92
  - MIT-LICENSE
93
93
  - README.md
94
94
  - Rakefile
95
+ - app/controllers/concerns/active_entry/concern.rb
96
+ - app/helpers/active_entry/view_helper.rb
95
97
  - lib/active_entry.rb
96
- - lib/active_entry/controller_methods.rb
98
+ - lib/active_entry/base.rb
97
99
  - lib/active_entry/errors.rb
100
+ - lib/active_entry/generators.rb
101
+ - lib/active_entry/policy_finder.rb
98
102
  - lib/active_entry/railtie.rb
103
+ - lib/active_entry/rspec.rb
99
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
100
114
  - lib/tasks/active_entry_tasks.rake
101
115
  homepage: https://github.com/TFM-Agency/active_entry
102
116
  licenses:
@@ -1,82 +0,0 @@
1
- # @author Tobias Feistmantl
2
- #
3
- # Helper methods for your controller
4
- # to identify RESTful actions.
5
- module ActiveEntry
6
- def method_missing method_name, *args
7
- method_name_str = method_name.to_s
8
-
9
- if methods.include?(:action_name) && method_name_str.include?("_action?")
10
- method_name_str.slice! "_action?"
11
-
12
- if methods.include? method_name_str.to_sym
13
- return method_name_str == action_name
14
- end
15
- end
16
-
17
- super
18
- end
19
-
20
- # @return [Boolean]
21
- # True if the called action
22
- # is a only-read action.
23
- def read_action?
24
- action_name == 'index' ||
25
- action_name == 'show'
26
- end
27
-
28
- # @return [Boolean]
29
- # True if the called action
30
- # is a write action.
31
- def write_action?
32
- action_name == 'new' ||
33
- action_name == 'create' ||
34
- action_name == 'edit' ||
35
- action_name == 'update' ||
36
- action_name == 'destroy'
37
- end
38
-
39
- # @return [Boolean]
40
- # True if the called action
41
- # is a change action.
42
- def change_action?
43
- action_name == 'edit' ||
44
- action_name == 'update' ||
45
- action_name == 'destroy'
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
- alias delete_action? destroy_action?
82
- end