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 +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +92 -10
- data/lib/authorizy/base_cop.rb +3 -3
- data/lib/authorizy/config.rb +2 -2
- data/lib/authorizy/core.rb +34 -21
- data/lib/authorizy/expander.rb +15 -16
- data/lib/authorizy/extension.rb +20 -5
- data/lib/authorizy/rspec.rb +44 -0
- data/lib/authorizy/version.rb +1 -1
- data/spec/authorizy/base_cop/access_question_spec.rb +2 -1
- data/spec/authorizy/config/aliases_spec.rb +2 -2
- data/spec/authorizy/config/cop_spec.rb +2 -2
- data/spec/authorizy/config/current_user_spec.rb +4 -4
- data/spec/authorizy/config/dependencies_spec.rb +2 -2
- data/spec/authorizy/config/initialize_spec.rb +1 -1
- data/spec/authorizy/config/redirect_url_spec.rb +4 -4
- data/spec/authorizy/cop/controller_spec.rb +1 -2
- data/spec/authorizy/cop/model_spec.rb +7 -6
- data/spec/authorizy/cop/namespaced_controller_spec.rb +1 -2
- data/spec/authorizy/core/access_spec.rb +57 -83
- data/spec/authorizy/expander/expand_spec.rb +41 -46
- data/spec/authorizy/extension/authorizy_question_spec.rb +14 -10
- data/spec/authorizy/extension/authorizy_spec.rb +14 -2
- data/spec/common_helper.rb +2 -0
- data/spec/spec_helper.rb +3 -3
- data/spec/support/coverage.rb +5 -1
- data/spec/support/models/authorizy_cop.rb +5 -5
- data/spec/support/schema.rb +1 -1
- metadata +69 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: faaf887b6dd9e95abc874df1b5ac534de5dc2dbebda5b2d887472a14a582fa9f
|
4
|
+
data.tar.gz: 156619514026194dc25299d689aec65ef0cce3ee5500c79090677286cb59dd61
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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/
|
7
|
-
[![Sponsor](https://img.shields.io/badge/
|
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
|
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
|
-
|
59
|
-
|
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
|
-
|
97
|
-
|
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,
|
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
|
+
```
|
data/lib/authorizy/base_cop.rb
CHANGED
@@ -2,9 +2,9 @@
|
|
2
2
|
|
3
3
|
module Authorizy
|
4
4
|
class BaseCop
|
5
|
-
def initialize(current_user, params, session
|
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
|
data/lib/authorizy/config.rb
CHANGED
@@ -7,9 +7,9 @@ module Authorizy
|
|
7
7
|
def initialize
|
8
8
|
@aliases = {}
|
9
9
|
@cop = Authorizy::BaseCop
|
10
|
-
@current_user = ->
|
10
|
+
@current_user = ->(context) { context.respond_to?(:current_user) ? context.current_user : nil }
|
11
11
|
@dependencies = {}
|
12
|
-
@redirect_url = ->
|
12
|
+
@redirect_url = ->(context) { context.respond_to?(:root_url) ? context.root_url : '/' }
|
13
13
|
end
|
14
14
|
end
|
15
15
|
end
|
data/lib/authorizy/core.rb
CHANGED
@@ -2,42 +2,55 @@
|
|
2
2
|
|
3
3
|
module Authorizy
|
4
4
|
class Core
|
5
|
-
def initialize(
|
6
|
-
@
|
7
|
-
@
|
8
|
-
@
|
9
|
-
@
|
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 @
|
13
|
+
return false if @user.blank?
|
15
14
|
|
16
|
-
|
17
|
-
|
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
|
-
|
20
|
-
end
|
19
|
+
return @cop.public_send(cop_controller) if @cop.respond_to?(cop_controller)
|
21
20
|
|
22
|
-
|
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
|
30
|
-
|
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
|
-
|
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
|
38
|
-
|
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
|
data/lib/authorizy/expander.rb
CHANGED
@@ -8,24 +8,27 @@ module Authorizy
|
|
8
8
|
result = {}
|
9
9
|
|
10
10
|
permissions.each do |permission|
|
11
|
-
|
11
|
+
controller = permission[0].to_s
|
12
|
+
action = permission[1].to_s
|
12
13
|
|
13
|
-
result[
|
14
|
+
result["#{controller}##{action}"] = [controller, action]
|
14
15
|
|
15
|
-
if (items = controller_dependency(
|
16
|
-
items.each
|
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[
|
22
|
+
actions = [default_aliases[action]].flatten.compact
|
20
23
|
|
21
24
|
next if actions.blank?
|
22
25
|
|
23
|
-
actions.each do |
|
24
|
-
result[
|
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(
|
38
|
-
return if (actions = dependencies[
|
39
|
-
return if (permissions = actions[
|
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 { |
|
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
|
data/lib/authorizy/extension.rb
CHANGED
@@ -8,23 +8,38 @@ module Authorizy
|
|
8
8
|
helper_method(:authorizy?)
|
9
9
|
|
10
10
|
def authorizy
|
11
|
-
return if
|
11
|
+
return if authorizy_core.new(authorizy_user, params, session, cop: authorizy_cop).access?
|
12
12
|
|
13
|
-
info = I18n.t('authorizy.denied',
|
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
|
17
|
+
redirect_to authorizy_config.redirect_url.call(self), info: info
|
18
18
|
end
|
19
19
|
|
20
20
|
def authorizy?(controller, action)
|
21
|
-
|
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
|
-
|
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
|