authorizy 0.1.0 → 0.2.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: d9be49c0763122d6a892671c240a9211720a664a62d66ee457c31203a436c0f8
4
- data.tar.gz: 8c6806a8c63b06c3f750cba5ed61ce512e8520b79d20273711cfdac1b1345188
3
+ metadata.gz: faaf887b6dd9e95abc874df1b5ac534de5dc2dbebda5b2d887472a14a582fa9f
4
+ data.tar.gz: 156619514026194dc25299d689aec65ef0cce3ee5500c79090677286cb59dd61
5
5
  SHA512:
6
- metadata.gz: 4aecc0fc9dfb238e9a0b4948bf49e424ae3bdd1ce9d8b5942c8ae66605c259b0f2f80b0800dcf6c383248a76a6a47ead49f42ff4f746523875eb8fb8e3c2a628
7
- data.tar.gz: 62386459e46f79d0d17a21e4229fd69c4915eb762f115adf2a76323590f0c471ef325bfa9f8c44ab6283a20e32dd196692da4958fd9a1f436255baafa83d1248
6
+ metadata.gz: 9d3e8128355ad5fc09b3bfd8f6719dc8b5ae1a11470de49979e8877f48c2f87752a7990549dc921758499585df4a6615f3941f91863afa65eb25520290577f81
7
+ data.tar.gz: 49f47d3b8d83d810a419139fb8c9419b51939bf817c6ec617200382e9951376c2ff545a669548d9f3ad6b44270facb849073ad80b5efa6f9348376b56c448df7
data/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ # v0.2.0
2
+
3
+ ## Break Changes
4
+
5
+ - The permissions format now is:
6
+
7
+ ```
8
+ {
9
+ permissions: [
10
+ ['controller', 'action'],
11
+ ['controller2', 'action2'],
12
+ ]
13
+ }
14
+ ```
15
+
16
+ ## Fixes
17
+
18
+ - Calls the `Authorizy::BaseCop#access?` as the first check intercepting all requests;
19
+
20
+ ## Features
21
+
22
+ - Added RSpec matcher to make the test easier;
23
+
1
24
  # v0.1.0
2
25
 
3
26
  ## Features
data/README.md CHANGED
@@ -3,8 +3,8 @@
3
3
  [![CI](https://github.com/wbotelhos/authorizy/workflows/CI/badge.svg)](https://github.com/wbotelhos/authorizy/actions)
4
4
  [![Gem Version](https://badge.fury.io/rb/authorizy.svg)](https://badge.fury.io/rb/authorizy)
5
5
  [![Maintainability](https://api.codeclimate.com/v1/badges/f312587b4f126bb13e85/maintainability)](https://codeclimate.com/github/wbotelhos/authorizy/maintainability)
6
- [![Coverage](https://codecov.io/gh/wbotelhos/blogy/branch/master/graph/badge.svg?token=PENDING)](https://codecov.io/gh/wbotelhos/authorizy)
7
- [![Sponsor](https://img.shields.io/badge/donate-%3C3-brightgreen.svg)](https://www.patreon.com/wbotelhos)
6
+ [![Coverage](https://codecov.io/gh/wbotelhos/authorizy/branch/main/graph/badge.svg)](https://codecov.io/gh/wbotelhos/authorizy)
7
+ [![Sponsor](https://img.shields.io/badge/sponsor-%3C3-green)](https://www.patreon.com/wbotelhos)
8
8
 
9
9
  A JSON based Authorization.
10
10
 
@@ -23,7 +23,7 @@ gem 'authorizy'
23
23
  Run the following task to create Authorizy migration and initialize.
24
24
 
25
25
  ```sh
26
- rails g rating:install
26
+ rails g authorizy:install
27
27
  ```
28
28
 
29
29
  Then execute the migration to adds the column `authorizy` to your `users` table.
@@ -55,8 +55,8 @@ The column `authorizy` is a JSON column that has a key called `permission` with
55
55
  ```ruby
56
56
  {
57
57
  permissions: [
58
- { controller: :user, action: :create },
59
- { controller: :user, action: :update },
58
+ [users, :create],
59
+ [users, :update],
60
60
  }
61
61
  }
62
62
  ```
@@ -93,8 +93,8 @@ Authorizy.configure do |config|
93
93
  config.dependencies = {
94
94
  payments: {
95
95
  index: [
96
- { controller: :users, action: :index },
97
- { controller: :enrollments, action: :index },
96
+ ['system/users', :index],
97
+ ['system/enrollments', :index],
98
98
  ]
99
99
  }
100
100
  }
@@ -105,7 +105,7 @@ So now if a have the permission `payments#index` I'll receive more two permissio
105
105
 
106
106
  ### Cop
107
107
 
108
- Sometimes we need to allow access in runtime because the permission will depend on the request data and/or some dynamic logic. For this you can create a *Cop* class, the inherit from `Authorizy::BaseCop`, to allow it based on logic. It works like a [Interceptor](https://en.wikipedia.org/wiki/Interceptor_pattern).
108
+ Sometimes we need to allow access in runtime because the permission will depend on the request data and/or some dynamic logic. For this you can create a *Cop* class, that inherits from `Authorizy::BaseCop`, to allow it based on logic. It works like a [Interceptor](https://en.wikipedia.org/wiki/Interceptor_pattern).
109
109
 
110
110
  First, you need to configure your cop:
111
111
 
@@ -130,6 +130,7 @@ end
130
130
  ```
131
131
 
132
132
  As you can see, you have access to a couple of variables: `action`, `controller`, `current_user`, `params`, and `session`.
133
+ When you return `false`, the authorization will be denied, when you return `true` your access will be allowed.
133
134
 
134
135
  If your controller has a namespace, just use `__` to separate the modules name:
135
136
 
@@ -140,13 +141,23 @@ class AuthorizyCop < Authorizy::BaseCop
140
141
  end
141
142
  ```
142
143
 
144
+ If you want to intercept all request as the first Authorizy check, you can override the `access?` method:
145
+
146
+ ```ruby
147
+ class AuthorizyCop < Authorizy::BaseCop
148
+ def access?
149
+ return true if current_user.admin?
150
+ end
151
+ end
152
+ ```
153
+
143
154
  ### Current User
144
155
 
145
156
  By default Authorizy fetch the current user from the variable `current_user`. You have a config, that receives the controller context, where you can change it:
146
157
 
147
158
  ```ruby
148
159
  Authorizy.configure do |config|
149
- config.current_user -> (context) { context.current_person }
160
+ config.current_user = -> (context) { context.current_person }
150
161
  end
151
162
  ```
152
163
 
@@ -156,7 +167,7 @@ When authorization fails and the request is not a XHR request a redirect happens
156
167
 
157
168
  ```ruby
158
169
  Authorizy.configure do |config|
159
- config.redirect_url -> (context) { context.new_session_url }
170
+ config.redirect_url = -> (context) { context.new_session_url }
160
171
  end
161
172
  ```
162
173
 
@@ -188,3 +199,74 @@ Using on jBuilder view:
188
199
  ```ruby
189
200
  json.create_link new_users_url if authorizy?(:users, :create)
190
201
  ```
202
+
203
+ # Specs
204
+
205
+ To test some routes you'll need to give or not permission to the user, for that you have to ways, where the first is give permission to the user via session:
206
+
207
+ ```ruby
208
+ before do
209
+ sign_in(current_user)
210
+
211
+ session[:permissions] = [[:users, :create]]
212
+ end
213
+ ```
214
+
215
+ Or you can put the permission directly in the current user:
216
+
217
+ ```ruby
218
+ before do
219
+ sign_in(current_user)
220
+
221
+ current_user.update(permissions: [[:users, :create]])
222
+ end
223
+ ```
224
+
225
+ ## Checks
226
+
227
+ We have a couple of check, here is the order:
228
+
229
+ 1. `Authorizy::BaseCop#access?`;
230
+ 2. `session[:permissions]`;
231
+ 3. `current_user.authorizy['permissions']`;
232
+ 4. `Authorizy::BaseCop#controller_name`;
233
+
234
+ ## Performance
235
+
236
+ If you have few permissions, you can save the permissions in the session and avoid hit database many times, but if you have a couple of them, maybe it's a good idea save it in some place like [Redis](https://redis.io).
237
+
238
+ ## Management
239
+
240
+ It's a good idea you keep your permissions in the database, so the customer can change it dynamic. You can load all permissions when the user is logged and cache it later. For cache expiration, you can trigger a refresh everytime that the permissions change.
241
+
242
+ ## Database Structure
243
+
244
+ Inside database you can use the following relation to dynamicly change your permissions:
245
+
246
+ ```ruby
247
+ plans -> plans_permissions <- permissions
248
+ |
249
+ v
250
+ role_plan_permissions
251
+ ^
252
+ |
253
+ roles
254
+ ```
255
+
256
+ ## RSpec
257
+
258
+ You can test you app passing through all authorizy layers:
259
+
260
+ ```ruby
261
+ user = User.create!(permission: { permissions: [[:users, :create]] })
262
+
263
+ expect(user).to be_authorized(:users, :create)
264
+ ```
265
+
266
+ Or make sure the user does not have access:
267
+
268
+ ```ruby
269
+ user = User.create!(permission: {})
270
+
271
+ expect(user).not_to be_authorized(:users, :create)
272
+ ```
@@ -2,9 +2,9 @@
2
2
 
3
3
  module Authorizy
4
4
  class BaseCop
5
- def initialize(current_user, params, session, controller, action)
6
- @action = action
7
- @controller = controller
5
+ def initialize(current_user, params, session)
6
+ @action = params[:action].to_s
7
+ @controller = params[:controller].to_s
8
8
  @current_user = current_user
9
9
  @params = params
10
10
  @session = session
@@ -7,9 +7,9 @@ module Authorizy
7
7
  def initialize
8
8
  @aliases = {}
9
9
  @cop = Authorizy::BaseCop
10
- @current_user = -> (context) { context.respond_to?(:current_user) ? context.current_user : nil }
10
+ @current_user = ->(context) { context.respond_to?(:current_user) ? context.current_user : nil }
11
11
  @dependencies = {}
12
- @redirect_url = -> (context) { context.respond_to?(:root_url) ? context.root_url : '/' }
12
+ @redirect_url = ->(context) { context.respond_to?(:root_url) ? context.root_url : '/' }
13
13
  end
14
14
  end
15
15
  end
@@ -2,42 +2,55 @@
2
2
 
3
3
  module Authorizy
4
4
  class Core
5
- def initialize(current_user, params, session, controller: params['controller'], action: params['action'])
6
- @action = action.to_s
7
- @controller = controller.to_s
8
- @current_user = current_user
9
- @params = params
10
- @session = session
5
+ def initialize(user, params, session, cop:)
6
+ @cop = cop
7
+ @params = params
8
+ @session = session
9
+ @user = user
11
10
  end
12
11
 
13
12
  def access?
14
- return false if @current_user.blank?
13
+ return false if @user.blank?
15
14
 
16
- granted = permissions.any? do |item|
17
- data = item.stringify_keys
15
+ return true if @cop.access? ||
16
+ session_permissions.any? { |tuple| route_match?(tuple) } ||
17
+ user_permissions.any? { |tuple| route_match?(tuple) }
18
18
 
19
- data['controller'].to_s == @controller && data['action'].to_s == @action
20
- end
19
+ return @cop.public_send(cop_controller) if @cop.respond_to?(cop_controller)
21
20
 
22
- return true if granted
23
-
24
- cop.respond_to?(cop_controller) && cop.public_send(cop_controller)
21
+ false
25
22
  end
26
23
 
27
24
  private
28
25
 
29
- def cop
30
- Authorizy.config.cop.new(@current_user, @params, @session, @controller, @action)
26
+ def action
27
+ @params[:action].to_s
28
+ end
29
+
30
+ def controller
31
+ @params[:controller].to_s
31
32
  end
32
33
 
33
34
  def cop_controller
34
- @controller.sub('/', '__')
35
+ controller.sub('/', '__')
36
+ end
37
+
38
+ def expand(permissions)
39
+ return [] if permissions.blank?
40
+
41
+ Authorizy::Expander.new.expand(permissions)
42
+ end
43
+
44
+ def session_permissions
45
+ expand(@session[:permissions])
46
+ end
47
+
48
+ def route_match?(tuple)
49
+ tuple[0] == controller && tuple[1] == action
35
50
  end
36
51
 
37
- def permissions
38
- Authorizy::Expander.new.expand(
39
- [@session['permissions']].flatten.compact.presence || @current_user.authorizy.try(:[], 'permissions')
40
- )
52
+ def user_permissions
53
+ expand(@user.authorizy.try(:[], 'permissions'))
41
54
  end
42
55
  end
43
56
  end
@@ -8,24 +8,27 @@ module Authorizy
8
8
  result = {}
9
9
 
10
10
  permissions.each do |permission|
11
- item = permission.stringify_keys.transform_values(&:to_s)
11
+ controller = permission[0].to_s
12
+ action = permission[1].to_s
12
13
 
13
- result[key_for(item)] = item
14
+ result["#{controller}##{action}"] = [controller, action]
14
15
 
15
- if (items = controller_dependency(item))
16
- items.each { |data| result[key_for(data)] = data }
16
+ if (items = controller_dependency(controller, action))
17
+ items.each do |controller_item, action_item|
18
+ result["#{controller_item}##{action_item}"] = [controller_item, action_item]
19
+ end
17
20
  end
18
21
 
19
- actions = [default_aliases[item['action']]].flatten.compact
22
+ actions = [default_aliases[action]].flatten.compact
20
23
 
21
24
  next if actions.blank?
22
25
 
23
- actions.each do |action|
24
- result[key_for(item, action: action)] = { 'action' => action.to_s, 'controller' => item['controller'].to_s }
26
+ actions.each do |action_item|
27
+ result["#{controller}##{action_item}"] = [controller, action_item.to_s]
25
28
  end
26
29
  end
27
30
 
28
- result.values
31
+ result.values # TODO: garantir o uniq
29
32
  end
30
33
 
31
34
  private
@@ -34,11 +37,11 @@ module Authorizy
34
37
  Authorizy.config.aliases.stringify_keys
35
38
  end
36
39
 
37
- def controller_dependency(item)
38
- return if (actions = dependencies[item['controller']]).blank?
39
- return if (permissions = actions[item['action']]).blank?
40
+ def controller_dependency(controller, action)
41
+ return if (actions = dependencies[controller]).blank?
42
+ return if (permissions = actions[action]).blank?
40
43
 
41
- permissions.map { |permission| permission.transform_values(&:to_s) }
44
+ permissions.map { |c, a| [c.to_s, a.to_s] }
42
45
  end
43
46
 
44
47
  def default_aliases
@@ -53,9 +56,5 @@ module Authorizy
53
56
  def dependencies
54
57
  Authorizy.config.dependencies.deep_stringify_keys
55
58
  end
56
-
57
- def key_for(item, action: nil)
58
- "#{item['controller']}##{action || item['action']}"
59
- end
60
59
  end
61
60
  end
@@ -8,23 +8,38 @@ module Authorizy
8
8
  helper_method(:authorizy?)
9
9
 
10
10
  def authorizy
11
- return if Authorizy::Core.new(authorizy_user, params, session).access?
11
+ return if authorizy_core.new(authorizy_user, params, session, cop: authorizy_cop).access?
12
12
 
13
- info = I18n.t('authorizy.denied', action: params[:action], controller: params[:controller])
13
+ info = I18n.t('authorizy.denied', controller: params[:controller], action: params[:action])
14
14
 
15
15
  return render(json: { message: info }, status: 422) if request.xhr?
16
16
 
17
- redirect_to Authorizy.config.redirect_url.call(self), info: info
17
+ redirect_to authorizy_config.redirect_url.call(self), info: info
18
18
  end
19
19
 
20
20
  def authorizy?(controller, action)
21
- Authorizy::Core.new(authorizy_user, params, session, action: action, controller: controller).access?
21
+ params['controller'] = controller
22
+ params['action'] = action
23
+
24
+ authorizy_core.new(authorizy_user, params, session, cop: authorizy_cop).access?
22
25
  end
23
26
 
24
27
  private
25
28
 
29
+ def authorizy_core
30
+ Authorizy::Core
31
+ end
32
+
26
33
  def authorizy_user
27
- Authorizy.config.current_user.call(self)
34
+ authorizy_config.current_user.call(self)
35
+ end
36
+
37
+ def authorizy_config
38
+ Authorizy.config
39
+ end
40
+
41
+ def authorizy_cop
42
+ authorizy_config.cop.new(authorizy_user, params, session)
28
43
  end
29
44
  end
30
45
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/expectations'
4
+
5
+ RSpec::Matchers.define :be_authorized do |controller, action, params: {}, session: {}|
6
+ match do |user|
7
+ parameters = params.merge(controller: controller, action: action)
8
+
9
+ access?(user, parameters, session)
10
+ end
11
+
12
+ match_when_negated do |user|
13
+ parameters = params.merge(controller: controller, action: action)
14
+
15
+ !access?(user, parameters, session)
16
+ end
17
+
18
+ failure_message do |user|
19
+ maybe_params_or_session("expected #{user.class}##{user.id} to be authorized in #{data}", params, session)
20
+ end
21
+
22
+ failure_message_when_negated do |user|
23
+ maybe_params_or_session("expected #{user.class}##{user.id} not to be authorized in #{data}", params, session)
24
+ end
25
+
26
+ private
27
+
28
+ def access?(user, params, session)
29
+ cop = Authorizy.config.cop.new(user, params, session)
30
+
31
+ Authorizy::Core.new(user, params, session, cop: cop).access?
32
+ end
33
+
34
+ def maybe_params_or_session(message, params, session)
35
+ message += ", params: #{params}" if params.present?
36
+ message += ", session: #{session}" if session.present?
37
+
38
+ message
39
+ end
40
+
41
+ def data
42
+ %(controller: "#{expected[0]}", action: "#{expected[1]}")
43
+ end
44
+ end