authorizy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE +21 -0
  4. data/README.md +190 -0
  5. data/lib/authorizy.rb +19 -0
  6. data/lib/authorizy/base_cop.rb +21 -0
  7. data/lib/authorizy/config.rb +15 -0
  8. data/lib/authorizy/core.rb +43 -0
  9. data/lib/authorizy/expander.rb +61 -0
  10. data/lib/authorizy/extension.rb +31 -0
  11. data/lib/authorizy/version.rb +5 -0
  12. data/lib/generators/authorizy/install_generator.rb +23 -0
  13. data/lib/generators/authorizy/templates/config/initializers/authorizy.rb +23 -0
  14. data/lib/generators/authorizy/templates/db/migrate/add_authorizy_on_users.rb +7 -0
  15. data/spec/authorizy/base_cop/access_question_spec.rb +9 -0
  16. data/spec/authorizy/config/aliases_spec.rb +13 -0
  17. data/spec/authorizy/config/cop_spec.rb +13 -0
  18. data/spec/authorizy/config/current_user_spec.rb +31 -0
  19. data/spec/authorizy/config/dependencies_spec.rb +13 -0
  20. data/spec/authorizy/config/initialize_spec.rb +7 -0
  21. data/spec/authorizy/config/redirect_url_spec.rb +31 -0
  22. data/spec/authorizy/cop/controller_spec.rb +42 -0
  23. data/spec/authorizy/cop/model_spec.rb +15 -0
  24. data/spec/authorizy/cop/namespaced_controller_spec.rb +42 -0
  25. data/spec/authorizy/core/access_spec.rb +137 -0
  26. data/spec/authorizy/expander/expand_spec.rb +144 -0
  27. data/spec/authorizy/extension/authorizy_question_spec.rb +46 -0
  28. data/spec/authorizy/extension/authorizy_spec.rb +56 -0
  29. data/spec/common_helper.rb +11 -0
  30. data/spec/spec_helper.rb +29 -0
  31. data/spec/support/application.rb +8 -0
  32. data/spec/support/common.rb +13 -0
  33. data/spec/support/controllers/admin/dummy_controller.rb +13 -0
  34. data/spec/support/controllers/dummy_controller.rb +11 -0
  35. data/spec/support/coverage.rb +14 -0
  36. data/spec/support/i18n.rb +3 -0
  37. data/spec/support/locales/en.yml +3 -0
  38. data/spec/support/models/authorizy_cop.rb +31 -0
  39. data/spec/support/models/empty_cop.rb +4 -0
  40. data/spec/support/models/user.rb +4 -0
  41. data/spec/support/routes.rb +6 -0
  42. data/spec/support/schema.rb +22 -0
  43. metadata +198 -0
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ Authorizy.configure do |config|
4
+ # Creates aliases to automatically allow permission for another action.
5
+ # https://github.com/wbotelhos/authorizy#aliases
6
+ # config.aliases = {}
7
+
8
+ # An interceptor to filter the request and decide if the request will be authorized
9
+ # https://github.com/wbotelhos/authorizy#cop
10
+ # config.cop = Authorizy::BaseCop
11
+
12
+ # The current user from we fetch the permissions
13
+ # https://github.com/wbotelhos/authorizy#current-user
14
+ # config.current_user = -> (context) { context.respond_to?(:current_user) ? context.current_user : nil }
15
+
16
+ # Inherited permissions from some other permission the user already has
17
+ # https://github.com/wbotelhos/authorizy#dependencies
18
+ # config.dependencies = {}
19
+
20
+ # URL to be redirect when user has no permission to access some resource
21
+ # https://github.com/wbotelhos/authorizy#dependencies
22
+ # config.redirect_url = -> (context) { context.respond_to?(:root_url) ? context.root_url : '/' }
23
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddAuthorizyOnUsers < ActiveRecord::Migration[6.0]
4
+ def change
5
+ add_column :users, :authorizy, :jsonb, default: {}, null: false
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Authorizy::BaseCop, '#access?' do
4
+ subject(:cop) { described_class.new('current_user', 'params', 'session', 'controller', 'action') }
5
+
6
+ it 'returns false as default' do
7
+ expect(cop.access?).to be(false)
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Authorizy::Config, '#aliases' do
4
+ subject(:config) { described_class.new }
5
+
6
+ it 'has default value and can receive a new one' do
7
+ expect(subject.aliases).to eq({})
8
+
9
+ config.aliases = 'value'
10
+
11
+ expect(config.aliases).to eq('value')
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Authorizy::Config, '#cop' do
4
+ subject(:config) { described_class.new }
5
+
6
+ it 'has default value and can receive a new one' do
7
+ expect(subject.cop).to eq(Authorizy::BaseCop)
8
+
9
+ config.cop = 'value'
10
+
11
+ expect(config.cop).to eq('value')
12
+ end
13
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Authorizy::Config, '#current_user' do
4
+ subject(:config) { described_class.new }
5
+
6
+ context 'when uses default value' do
7
+ context 'when context responds to current_user' do
8
+ let!(:context) { OpenStruct.new(current_user: 'user') }
9
+
10
+ it 'is called' do
11
+ expect(subject.current_user.call(context)).to eq('user')
12
+ end
13
+ end
14
+
15
+ context 'when context does not respond to current_user' do
16
+ let!(:context) { 'context' }
17
+
18
+ it 'returns nil' do
19
+ expect(subject.current_user.call(context)).to be(nil)
20
+ end
21
+ end
22
+ end
23
+
24
+ context 'when uses custom value' do
25
+ it 'executes what you want' do
26
+ config.current_user = -> (context) { context[:value] }
27
+
28
+ expect(config.current_user.call({ value: 'value' })).to eq('value')
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Authorizy::Config, '#dependencies' do
4
+ subject(:config) { described_class.new }
5
+
6
+ it 'has default value and can receive a new one' do
7
+ expect(subject.dependencies).to eq({})
8
+
9
+ config.dependencies = 'value'
10
+
11
+ expect(config.dependencies).to eq('value')
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Authorizy::Config do
4
+ it 'starts with a default cop' do
5
+ expect(subject.cop).to eq(Authorizy::BaseCop)
6
+ end
7
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Authorizy::Config, '#redirect_url' do
4
+ subject(:config) { described_class.new }
5
+
6
+ context 'when uses default value' do
7
+ context 'when context responds to root_url' do
8
+ let!(:context) { OpenStruct.new(root_url: '/root') }
9
+
10
+ it 'is called' do
11
+ expect(subject.redirect_url.call(context)).to eq('/root')
12
+ end
13
+ end
14
+
15
+ context 'when context does not respond to root_url' do
16
+ let!(:context) { 'context' }
17
+
18
+ it 'returns just a slash' do
19
+ expect(subject.redirect_url.call(context)).to eq('/')
20
+ end
21
+ end
22
+ end
23
+
24
+ context 'when uses custom value' do
25
+ it 'executes what you want' do
26
+ config.redirect_url = -> (context) { context[:value] }
27
+
28
+ expect(config.redirect_url.call({ value: 'value' })).to eq('value')
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'support/models/authorizy_cop'
4
+ require 'support/models/empty_cop'
5
+ require 'support/controllers/dummy_controller'
6
+
7
+ RSpec.describe DummyController, '#authorizy', type: :controller do
8
+ let!(:parameters) { ActionController::Parameters.new(key: 'value', controller: 'dummy', action: 'action') }
9
+ let!(:user) { User.new }
10
+
11
+ context 'when cop responds to the controller name' do
12
+ context 'when method resturns false' do
13
+ it 'denies the access' do
14
+ config_mock(cop: AuthorizyCop, current_user: user) do
15
+ get :action, params: { access: false }
16
+ end
17
+
18
+ expect(response).to redirect_to('/')
19
+ end
20
+ end
21
+
22
+ context 'when method resturns true' do
23
+ it 'denies the access' do
24
+ config_mock(cop: AuthorizyCop, current_user: user) do
25
+ get :action, params: { access: true }
26
+ end
27
+
28
+ expect(response.body).to eq('{"message":"authorized"}')
29
+ end
30
+ end
31
+ end
32
+
33
+ context 'when cop responds to the controller name' do
34
+ it 'denies the access' do
35
+ config_mock(cop: EmptyCop, current_user: user) do
36
+ get :action
37
+ end
38
+
39
+ expect(response).to redirect_to('/')
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'support/models/authorizy_cop'
4
+
5
+ RSpec.describe AuthorizyCop do
6
+ subject(:cop) { described_class.new('current_user', 'params', 'session', 'controller', 'action') }
7
+
8
+ it 'adds private attributes readers' do
9
+ expect(cop.get_action).to eq('action')
10
+ expect(cop.get_controller).to eq('controller')
11
+ expect(cop.get_current_user).to eq('current_user')
12
+ expect(cop.get_params).to eq('params')
13
+ expect(cop.get_session).to eq('session')
14
+ end
15
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'support/models/authorizy_cop'
4
+ require 'support/models/empty_cop'
5
+ require 'support/controllers/admin/dummy_controller'
6
+
7
+ RSpec.describe Admin::DummyController, '#authorizy', type: :controller do
8
+ let!(:parameters) { ActionController::Parameters.new(key: 'value', controller: 'admin/users', action: 'action') }
9
+ let!(:user) { User.new }
10
+
11
+ context 'when cop responds to the controller name' do
12
+ context 'when method resturns false' do
13
+ it 'denies the access' do
14
+ config_mock(cop: AuthorizyCop, current_user: user) do
15
+ get :action, params: { admin: false }
16
+ end
17
+
18
+ expect(response).to redirect_to('/')
19
+ end
20
+ end
21
+
22
+ context 'when method resturns true' do
23
+ it 'denies the access' do
24
+ config_mock(cop: AuthorizyCop, current_user: user) do
25
+ get :action, params: { admin: true }
26
+ end
27
+
28
+ expect(response.body).to eq('{"message":"authorized"}')
29
+ end
30
+ end
31
+ end
32
+
33
+ context 'when cop responds to the controller name' do
34
+ it 'denies the access' do
35
+ config_mock(cop: EmptyCop, current_user: user) do
36
+ get :action
37
+ end
38
+
39
+ expect(response).to redirect_to('/')
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Authorizy::Core, '#access?' do
4
+ context 'when permissions is in session as string' do
5
+ let!(:current_user) { User.new }
6
+ let!(:params) { { 'action' => 'create', 'controller' => 'controller' } }
7
+ let!(:session) { { 'permissions' => [{ 'action' => 'create', 'controller' => 'controller' }] } }
8
+
9
+ it 'uses the session value skipping the user fetch' do
10
+ expect(described_class.new(current_user, params, session).access?).to be(true)
11
+ end
12
+ end
13
+
14
+ context 'when permissions is not in session' do
15
+ subject(:authorizy) { described_class.new(current_user, params, session) }
16
+
17
+ let!(:current_user) { User.new(authorizy: { permissions: [{ action: 'create', controller: 'match' }] }) }
18
+ let!(:params) { { 'action' => 'create', 'controller' => 'match' } }
19
+ let!(:session) { {} }
20
+
21
+ it 'fetches the permission from user' do
22
+ expect(authorizy.access?).to be(true)
23
+ end
24
+ end
25
+
26
+ context 'when session has no permission nor the user' do
27
+ subject(:authorizy) { described_class.new(current_user, params, session) }
28
+
29
+ let!(:current_user) { User.new }
30
+ let!(:params) { { 'action' => 'create', 'controller' => 'match' } }
31
+ let!(:session) { {} }
32
+
33
+ it { expect(authorizy.access?).to be(false) }
34
+ end
35
+
36
+ context 'when cop does not respond to controller' do
37
+ subject(:authorizy) { described_class.new(current_user, params, session) }
38
+
39
+ let!(:cop) { instance_double('Authorizy.config.cop') }
40
+ let!(:current_user) { User.new }
41
+ let!(:params) { { 'action' => 'create', 'controller' => 'missing' } }
42
+ let!(:session) { {} }
43
+
44
+ before do
45
+ allow(Authorizy.config.cop).to receive(:new)
46
+ .with(current_user, params, session, 'missing', 'create')
47
+ .and_return(cop)
48
+
49
+ allow(cop).to receive(:respond_to?).with('missing').and_return(false)
50
+ end
51
+
52
+ it 'does not authorize via cop' do
53
+ expect(authorizy.access?).to be(false)
54
+ end
55
+ end
56
+
57
+ context 'when cop responds to controller' do
58
+ subject(:authorizy) { described_class.new(current_user, params, session) }
59
+
60
+ let!(:cop) { instance_double('Authorizy.config.cop') }
61
+ let!(:current_user) { User.new }
62
+ let!(:params) { { 'action' => 'create', 'controller' => 'match' } }
63
+ let!(:session) { {} }
64
+
65
+ before do
66
+ allow(Authorizy.config.cop).to receive(:new)
67
+ .with(current_user, params, session, 'match', 'create')
68
+ .and_return(cop)
69
+
70
+ allow(cop).to receive(:respond_to?).with('match').and_return(true)
71
+ end
72
+
73
+ context 'when cop does not release the access' do
74
+ it 'continues trying via session and so user permissions' do
75
+ allow(cop).to receive(:public_send).with('match').and_return(false)
76
+
77
+ expect(authorizy.access?).to be(false)
78
+ end
79
+ end
80
+
81
+ context 'when cop releases the access' do
82
+ it 'skips session and user permission return true to the access' do
83
+ allow(cop).to receive(:public_send).with('match').and_return(true)
84
+
85
+ expect(authorizy.access?).to be(true)
86
+ end
87
+ end
88
+ end
89
+
90
+ context 'when controller is given' do
91
+ subject(:authorizy) { described_class.new(current_user, params, session, controller: 'controller') }
92
+
93
+ let!(:current_user) { User.new }
94
+ let!(:params) { { 'action' => 'action', 'controller' => 'ignored' } }
95
+ let!(:session) { { 'permissions' => [{ 'action' => 'action', 'controller' => 'controller' }] } }
96
+
97
+ it 'uses the given controller over the one on params' do
98
+ expect(authorizy.access?).to be(true)
99
+ end
100
+ end
101
+
102
+ context 'when action is given' do
103
+ subject(:authorizy) { described_class.new(current_user, params, session, action: 'action') }
104
+
105
+ let!(:current_user) { User.new }
106
+ let!(:params) { { 'action' => 'ignored', 'controller' => 'controller' } }
107
+ let!(:session) { { 'permissions' => [{ 'action' => 'action', 'controller' => 'controller' }] } }
108
+
109
+ it 'uses the given action over the one on params' do
110
+ expect(authorizy.access?).to be(true)
111
+ end
112
+ end
113
+
114
+ context 'when user has the controller permission but not action' do
115
+ subject(:authorizy) { described_class.new(current_user, params, session) }
116
+
117
+ let!(:current_user) { User.new }
118
+ let!(:params) { { 'action' => 'action', 'controller' => 'controller' } }
119
+ let!(:session) { { 'permissions' => [{ 'action' => 'miss', 'controller' => 'controller' }] } }
120
+
121
+ it 'cannot access' do
122
+ expect(authorizy.access?).to be(false)
123
+ end
124
+ end
125
+
126
+ context 'when user has the action permission but not controller' do
127
+ subject(:authorizy) { described_class.new(current_user, params, session) }
128
+
129
+ let!(:current_user) { User.new }
130
+ let!(:params) { { 'action' => 'action', 'controller' => 'controller' } }
131
+ let!(:session) { { 'permissions' => [{ 'action' => 'create', 'controller' => 'miss' }] } }
132
+
133
+ it 'cannot access' do
134
+ expect(authorizy.access?).to be(false)
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Authorizy::Expander, '#expand' do
4
+ subject(:expander) { described_class.new }
5
+
6
+ context 'when permissions is blank' do
7
+ let(:permissions) { [] }
8
+
9
+ it 'returns an empty permissions' do
10
+ expect(expander.expand(permissions)).to eq []
11
+ end
12
+ end
13
+
14
+ context 'when permissions is given' do
15
+ context 'when data is symbol' do
16
+ let(:permissions) do
17
+ [
18
+ { action: :create, controller: :controller },
19
+ { action: :edit, controller: :controller },
20
+ { action: :new, controller: :controller },
21
+ { action: :update, controller: :controller },
22
+ ]
23
+ end
24
+
25
+ it 'mappes the default actions aliases' do
26
+ expect(expander.expand(permissions)).to match_array [
27
+ { 'action' => 'create', 'controller' => 'controller' },
28
+ { 'action' => 'edit', 'controller' => 'controller' },
29
+ { 'action' => 'new', 'controller' => 'controller' },
30
+ { 'action' => 'update', 'controller' => 'controller' },
31
+ ]
32
+ end
33
+ end
34
+
35
+ context 'when data is string' do
36
+ let(:permissions) do
37
+ [
38
+ { 'action' => 'create', 'controller' => 'controller' },
39
+ { 'action' => 'edit', 'controller' => 'controller' },
40
+ { 'action' => 'new', 'controller' => 'controller' },
41
+ { 'action' => 'update', 'controller' => 'controller' },
42
+ ]
43
+ end
44
+
45
+ it 'mappes the default actions aliases' do
46
+ expect(expander.expand(permissions)).to match_array [
47
+ { 'action' => 'create', 'controller' => 'controller' },
48
+ { 'action' => 'edit', 'controller' => 'controller' },
49
+ { 'action' => 'new', 'controller' => 'controller' },
50
+ { 'action' => 'update', 'controller' => 'controller' },
51
+ ]
52
+ end
53
+ end
54
+ end
55
+
56
+ context 'when a dependencies is given' do
57
+ context 'when keys and values are strings' do
58
+ let(:dependencies) { { 'controller' => { 'action' => [{ 'action' => 'action2', 'controller' => 'controller2' }] } } }
59
+ let!(:permissions) { [{ 'action' => 'action', 'controller' => 'controller' }] }
60
+
61
+ it 'addes the dependencies permissions' do
62
+ config_mock(dependencies: dependencies) do
63
+ expect(expander.expand(permissions)).to match_array [
64
+ { 'action' => 'action', 'controller' => 'controller' },
65
+ { 'action' => 'action2', 'controller' => 'controller2' },
66
+ ]
67
+ end
68
+ end
69
+ end
70
+
71
+ context 'when keys and values are symbol' do
72
+ let(:dependencies) { { controller: { action: [{ action: :action2, controller: :controller2 }] } } }
73
+ let!(:permissions) { [{ 'action' => 'action', 'controller' => 'controller' }] }
74
+
75
+ it 'addes the dependencies permissions' do
76
+ config_mock(dependencies: dependencies) do
77
+ expect(expander.expand(permissions)).to match_array [
78
+ { 'action' => 'action', 'controller' => 'controller' },
79
+ { 'action' => 'action2', 'controller' => 'controller2' },
80
+ ]
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+
87
+ context 'when aliases is given' do
88
+ let!(:permissions) { [{ 'action' => 'action', 'controller' => 'controller' }] }
89
+
90
+ context 'when key and values are strings' do
91
+ let(:aliases) { { 'action' => 'action2' } }
92
+
93
+ it 'mappes the action with the current controller' do
94
+ config_mock(aliases: aliases) do
95
+ expect(expander.expand(permissions)).to match_array [
96
+ { 'action' => 'action', 'controller' => 'controller' },
97
+ { 'action' => 'action2', 'controller' => 'controller' },
98
+ ]
99
+ end
100
+ end
101
+ end
102
+
103
+ context 'when key and values are symbols' do
104
+ let(:aliases) { { action: :action2 } }
105
+
106
+ it 'mappes the action with the current controller' do
107
+ config_mock(aliases: aliases) do
108
+ expect(expander.expand(permissions)).to match_array [
109
+ { 'action' => 'action', 'controller' => 'controller' },
110
+ { 'action' => 'action2', 'controller' => 'controller' },
111
+ ]
112
+ end
113
+ end
114
+ end
115
+
116
+ context 'when key and values are array of strings' do
117
+ let(:aliases) { { action: %w[action2 action3] } }
118
+
119
+ it 'mappes the actions with the current controller' do
120
+ config_mock(aliases: aliases) do
121
+ expect(expander.expand(permissions)).to match_array [
122
+ { 'action' => 'action', 'controller' => 'controller' },
123
+ { 'action' => 'action2', 'controller' => 'controller' },
124
+ { 'action' => 'action3', 'controller' => 'controller' },
125
+ ]
126
+ end
127
+ end
128
+ end
129
+
130
+ context 'when key and values are array of symbols' do
131
+ let(:aliases) { { action: %i[action2 action3] } }
132
+
133
+ it 'mappes the actions with the current controller' do
134
+ config_mock(aliases: aliases) do
135
+ expect(expander.expand(permissions)).to match_array [
136
+ { 'action' => 'action', 'controller' => 'controller' },
137
+ { 'action' => 'action2', 'controller' => 'controller' },
138
+ { 'action' => 'action3', 'controller' => 'controller' },
139
+ ]
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end