forest_liana 5.3.2 → 6.0.0.pre.beta.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/forest_liana/actions_controller.rb +8 -1
  3. data/app/controllers/forest_liana/application_controller.rb +1 -7
  4. data/app/controllers/forest_liana/authentication_controller.rb +122 -0
  5. data/app/controllers/forest_liana/base_controller.rb +4 -0
  6. data/app/controllers/forest_liana/resources_controller.rb +14 -17
  7. data/app/controllers/forest_liana/router.rb +2 -2
  8. data/app/controllers/forest_liana/sessions_controller.rb +1 -1
  9. data/app/controllers/forest_liana/smart_actions_controller.rb +10 -5
  10. data/app/controllers/forest_liana/stats_controller.rb +5 -5
  11. data/app/helpers/forest_liana/adapter_helper.rb +1 -1
  12. data/app/serializers/forest_liana/schema_serializer.rb +2 -2
  13. data/app/services/forest_liana/apimap_sorter.rb +1 -1
  14. data/app/services/forest_liana/authentication.rb +59 -0
  15. data/app/services/forest_liana/authorization_getter.rb +12 -20
  16. data/app/services/forest_liana/forest_api_requester.rb +14 -5
  17. data/app/services/forest_liana/ip_whitelist_checker.rb +1 -1
  18. data/app/services/forest_liana/login_handler.rb +3 -11
  19. data/app/services/forest_liana/oidc_client_manager.rb +34 -0
  20. data/app/services/forest_liana/oidc_configuration_retriever.rb +12 -0
  21. data/app/services/forest_liana/oidc_dynamic_client_registrator.rb +67 -0
  22. data/app/services/forest_liana/permissions_checker.rb +117 -56
  23. data/app/services/forest_liana/permissions_formatter.rb +52 -0
  24. data/app/services/forest_liana/permissions_getter.rb +52 -17
  25. data/app/services/forest_liana/query_stat_getter.rb +5 -5
  26. data/app/services/forest_liana/scope_validator.rb +8 -7
  27. data/app/services/forest_liana/token.rb +27 -0
  28. data/app/services/forest_liana/utils/beta_schema_utils.rb +13 -0
  29. data/config/initializers/error-messages.rb +20 -0
  30. data/config/routes.rb +5 -0
  31. data/lib/forest_liana.rb +1 -0
  32. data/lib/forest_liana/bootstrapper.rb +1 -1
  33. data/lib/forest_liana/collection.rb +2 -2
  34. data/lib/forest_liana/engine.rb +9 -0
  35. data/lib/forest_liana/json_printer.rb +1 -1
  36. data/lib/forest_liana/version.rb +1 -1
  37. data/spec/dummy/app/assets/config/manifest.js +1 -0
  38. data/spec/dummy/config/application.rb +1 -1
  39. data/spec/dummy/config/initializers/forest_liana.rb +1 -0
  40. data/spec/dummy/db/migrate/20190226172951_create_user.rb +1 -1
  41. data/spec/dummy/db/migrate/20190226173051_create_isle.rb +1 -1
  42. data/spec/dummy/db/migrate/20190226174951_create_tree.rb +1 -1
  43. data/spec/dummy/db/migrate/20190716130830_add_age_to_tree.rb +1 -1
  44. data/spec/dummy/db/migrate/20190716135241_add_type_to_user.rb +1 -1
  45. data/spec/dummy/db/schema.rb +18 -20
  46. data/spec/requests/actions_controller_spec.rb +46 -11
  47. data/spec/requests/authentications_spec.rb +105 -0
  48. data/spec/requests/resources_spec.rb +4 -4
  49. data/spec/requests/sessions_spec.rb +53 -0
  50. data/spec/services/forest_liana/permissions_checker_acl_disabled_spec.rb +711 -0
  51. data/spec/services/forest_liana/permissions_checker_acl_enabled_spec.rb +831 -0
  52. data/spec/services/forest_liana/permissions_formatter_spec.rb +222 -0
  53. data/spec/services/forest_liana/permissions_getter_spec.rb +83 -0
  54. data/spec/spec_helper.rb +3 -0
  55. data/test/dummy/app/assets/config/manifest.js +1 -0
  56. data/test/dummy/config/application.rb +1 -1
  57. data/test/dummy/db/migrate/20150608130516_create_date_field.rb +1 -1
  58. data/test/dummy/db/migrate/20150608131430_create_integer_field.rb +1 -1
  59. data/test/dummy/db/migrate/20150608131603_create_decimal_field.rb +1 -1
  60. data/test/dummy/db/migrate/20150608131610_create_float_field.rb +1 -1
  61. data/test/dummy/db/migrate/20150608132159_create_boolean_field.rb +1 -1
  62. data/test/dummy/db/migrate/20150608132621_create_string_field.rb +1 -1
  63. data/test/dummy/db/migrate/20150608133038_create_belongs_to_field.rb +1 -1
  64. data/test/dummy/db/migrate/20150608133044_create_has_one_field.rb +1 -1
  65. data/test/dummy/db/migrate/20150608150016_create_has_many_field.rb +1 -1
  66. data/test/dummy/db/migrate/20150609114636_create_belongs_to_class_name_field.rb +1 -1
  67. data/test/dummy/db/migrate/20150612112520_create_has_and_belongs_to_many_field.rb +1 -1
  68. data/test/dummy/db/migrate/20150616150629_create_polymorphic_field.rb +1 -1
  69. data/test/dummy/db/migrate/20150623115554_create_has_many_class_name_field.rb +1 -1
  70. data/test/dummy/db/migrate/20150814081918_create_has_many_through_field.rb +1 -1
  71. data/test/dummy/db/migrate/20160627172810_create_owner.rb +1 -1
  72. data/test/dummy/db/migrate/20160627172951_create_tree.rb +1 -1
  73. data/test/dummy/db/migrate/20160628173505_add_timestamps.rb +1 -1
  74. data/test/dummy/db/migrate/20170614141921_create_serialize_field.rb +1 -1
  75. data/test/dummy/db/migrate/20181111162121_create_references_table.rb +1 -1
  76. metadata +71 -4
@@ -30,6 +30,12 @@ describe 'Requesting Actions routes', :type => :request do
30
30
  description: nil,
31
31
  widget: nil,
32
32
  }
33
+ enum = {
34
+ field: 'enum',
35
+ type: 'Enum',
36
+ enums: %w[a b c],
37
+ }
38
+
33
39
  action_definition = {
34
40
  name: 'my_action',
35
41
  fields: [foo],
@@ -76,33 +82,47 @@ describe 'Requesting Actions routes', :type => :request do
76
82
  }
77
83
  }
78
84
  }
85
+ enums_action_definition = {
86
+ name: 'enums_action',
87
+ fields: [foo, enum],
88
+ hooks: {
89
+ :change => {
90
+ 'foo' => -> (context) {
91
+ fields = context[:fields]
92
+ fields['enum'][:enums] = %w[c d e]
93
+ return fields
94
+ }
95
+ }
96
+ }
97
+ }
79
98
  action = ForestLiana::Model::Action.new(action_definition)
80
99
  fail_action = ForestLiana::Model::Action.new(fail_action_definition)
81
100
  cheat_action = ForestLiana::Model::Action.new(cheat_action_definition)
101
+ enums_action = ForestLiana::Model::Action.new(enums_action_definition)
82
102
  island = ForestLiana.apimap.find {|collection| collection.name.to_s == ForestLiana.name_for(Island)}
83
- island.actions = [action, fail_action, cheat_action]
103
+ island.actions = [action, fail_action, cheat_action, enums_action]
84
104
 
85
105
  describe 'call /load' do
86
106
  params = {recordIds: [1], collectionName: 'Island'}
87
107
 
88
108
  it 'should respond 200' do
89
- post '/forest/actions/my_action/hooks/load', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
109
+ post '/forest/actions/my_action/hooks/load', params: JSON.dump(params), headers: { 'CONTENT_TYPE' => 'application/json' }
90
110
  expect(response.status).to eq(200)
91
111
  expect(JSON.parse(response.body)).to eq({'fields' => [foo.merge({:value => nil}).stringify_keys]})
92
112
  end
93
113
 
94
114
  it 'should respond 500 with bad params' do
95
- post '/forest/actions/my_action/hooks/load', {}
115
+ post '/forest/actions/my_action/hooks/load', params: {}
96
116
  expect(response.status).to eq(500)
97
117
  end
98
118
 
99
119
  it 'should respond 500 with bad hook result type' do
100
- post '/forest/actions/fail_action/hooks/load', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
120
+ post '/forest/actions/fail_action/hooks/load', params: JSON.dump(params), headers: { 'CONTENT_TYPE' => 'application/json' }
101
121
  expect(response.status).to eq(500)
102
122
  end
103
123
 
104
124
  it 'should respond 500 with bad hook result data structure' do
105
- post '/forest/actions/cheat_action/hooks/load', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
125
+ post '/forest/actions/cheat_action/hooks/load', params: JSON.dump(params), headers: { 'CONTENT_TYPE' => 'application/json' }
106
126
  expect(response.status).to eq(500)
107
127
  end
108
128
  end
@@ -112,28 +132,43 @@ describe 'Requesting Actions routes', :type => :request do
112
132
  params = {recordIds: [1], fields: [updated_foo], collectionName: 'Island', changedField: 'foo'}
113
133
 
114
134
  it 'should respond 200' do
115
- post '/forest/actions/my_action/hooks/change', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
135
+ post '/forest/actions/my_action/hooks/change', params: JSON.dump(params), headers: { 'CONTENT_TYPE' => 'application/json' }
116
136
  expect(response.status).to eq(200)
117
- expected = updated_foo.merge({:value => 'baz'})
137
+ expected = updated_foo.clone.merge({:value => 'baz'})
118
138
  expected[:widgetEdit] = nil
119
139
  expected.delete(:widget)
120
140
  expect(JSON.parse(response.body)).to eq({'fields' => [expected.stringify_keys]})
121
141
  end
122
142
 
123
143
  it 'should respond 500 with bad params' do
124
- post '/forest/actions/my_action/hooks/change', {}
144
+ post '/forest/actions/my_action/hooks/change', params: {}
125
145
  expect(response.status).to eq(500)
126
146
  end
127
147
 
128
148
  it 'should respond 500 with bad hook result type' do
129
- post '/forest/actions/fail_action/hooks/change', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
149
+ post '/forest/actions/fail_action/hooks/change', params: JSON.dump(params), headers: { 'CONTENT_TYPE' => 'application/json' }
130
150
  expect(response.status).to eq(500)
131
151
  end
132
152
 
133
153
  it 'should respond 500 with bad hook result data structure' do
134
- post '/forest/actions/cheat_action/hooks/change', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
154
+ post '/forest/actions/cheat_action/hooks/change', params: JSON.dump(params), headers: { 'CONTENT_TYPE' => 'application/json' }
135
155
  expect(response.status).to eq(500)
136
156
  end
157
+
158
+ it 'should reset value when enums has changed' do
159
+ updated_enum = enum.clone.merge({:previousValue => nil, :value => 'a'}) # set value to a
160
+ p = {recordIds: [1], fields: [updated_foo, updated_enum], collectionName: 'Island', changedField: 'foo'}
161
+ post '/forest/actions/enums_action/hooks/change', params: JSON.dump(p), headers: { 'CONTENT_TYPE' => 'application/json' }
162
+ expect(response.status).to eq(200)
163
+
164
+ expected_enum = updated_enum.clone.merge({ :enums => %w[c d e], :value => nil, :widgetEdit => nil})
165
+ expected_enum.delete(:widget)
166
+ expected_foo = updated_foo.clone.merge({ :widgetEdit => nil})
167
+ expected_foo.delete(:widget)
168
+
169
+ expect(JSON.parse(response.body)).to eq({'fields' => [expected_foo.stringify_keys, expected_enum.stringify_keys]})
170
+ end
171
+
137
172
  end
138
173
  end
139
- end
174
+ end
@@ -0,0 +1,105 @@
1
+ require 'rails_helper'
2
+ require 'openid_connect'
3
+ require 'json'
4
+
5
+ describe "Authentications", type: :request do
6
+ before do
7
+ allow(ForestLiana::IpWhitelist).to receive(:retrieve) { true }
8
+ allow(ForestLiana::IpWhitelist).to receive(:is_ip_whitelist_retrieved) { true }
9
+ allow(ForestLiana::IpWhitelist).to receive(:is_ip_valid) { true }
10
+ allow(ForestLiana::OidcConfigurationRetriever).to receive(:retrieve) {
11
+ JSON.parse('{
12
+ "registration_endpoint": "https://api.forestadmin.com/oidc/registration",
13
+ "issuer": "api.forestadmin.com"
14
+ }', :symbolize_names => false)
15
+ }
16
+ allow(ForestLiana::ForestApiRequester).to receive(:post) {
17
+ instance_double(HTTParty::Response, body: '{ "client_id": "random_id" }', code: 201)
18
+ }
19
+ allow_any_instance_of(OpenIDConnect::Client).to receive(:access_token!) {
20
+ OpenIDConnect::AccessToken.new(access_token: 'THE-ACCESS-TOKEN', client: instance_double(OpenIDConnect::Client))
21
+ }
22
+ end
23
+
24
+ after do
25
+ Rails.cache.delete(URI.join(ForestLiana.application_url, ForestLiana::Engine.routes.url_helpers.authentication_callback_path).to_s)
26
+ end
27
+
28
+ describe "POST /authentication" do
29
+ before() do
30
+ post ForestLiana::Engine.routes.url_helpers.authentication_path, params: '{"renderingId":"42"}', headers: {
31
+ 'Accept' => 'application/json',
32
+ 'Content-Type' => 'application/json',
33
+ }
34
+ end
35
+
36
+ it "should respond with a 302 code" do
37
+ expect(response).to have_http_status(302)
38
+ end
39
+
40
+ it "should return a valid authentication url" do
41
+ expect(response.headers['Location']).to eq('https://api.forestadmin.com/oidc/auth?client_id=random_id&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fforest%2Fauthentication%2Fcallback&response_type=code&scope=openid%20email%20profile&state=%7B%22renderingId%22%3D%3E42%7D')
42
+ end
43
+ end
44
+
45
+ describe "GET /authentication/callback" do
46
+ before() do
47
+ response = '{"data":{"id":666,"attributes":{"first_name":"Alice","last_name":"Doe","email":"alice@forestadmin.com","teams":[1,2,3]}}}'
48
+ allow(ForestLiana::ForestApiRequester).to receive(:get).with(
49
+ "/liana/v2/renderings/42/authorization", { :headers => { "forest-token" => "THE-ACCESS-TOKEN" }, :query=> {} }
50
+ ).and_return(
51
+ instance_double(HTTParty::Response, :body => response, :code => 200)
52
+ )
53
+
54
+ get ForestLiana::Engine.routes.url_helpers.authentication_callback_path + "?code=THE-CODE&state=#{CGI::escape('{"renderingId":42}')}"
55
+ end
56
+
57
+ it "should respond with a 200 code" do
58
+ expect(response).to have_http_status(200)
59
+ end
60
+
61
+ it "should return a valid authentication token" do
62
+ session_cookie = response.headers['set-cookie']
63
+ expect(session_cookie).to match(/^forest_session_token=[^;]+; path=\/; expires=[^;]+; secure; HttpOnly$/)
64
+
65
+ token = session_cookie.match(/^forest_session_token=([^;]+);/)[1]
66
+ decoded = JWT.decode(token, ForestLiana.auth_secret, true, { algorithm: 'HS256' })[0]
67
+
68
+ expected_token_data = {
69
+ "id" => 666,
70
+ "email" => 'alice@forestadmin.com',
71
+ "rendering_id" => "42",
72
+ "first_name" => 'Alice',
73
+ "last_name" => 'Doe',
74
+ "team" => 1,
75
+ }
76
+
77
+ expect(decoded).to include(expected_token_data)
78
+ expect(JSON.parse(response.body, :symbolize_names => true)).to eq({ token: token, tokenData: decoded.deep_symbolize_keys! })
79
+ expect(response).to have_http_status(200)
80
+ end
81
+ end
82
+
83
+ describe "POST /authentication/logout" do
84
+ before() do
85
+ cookies['forest_session_token'] = {
86
+ value: 'eyJhbGciOiJIUzI1NiJ9.eyJpZCI6NjY2LCJlbWFpbCI6ImFsaWNlQGZvcmVzdGFkbWluLmNvbSIsImZpcnN0X25hbWUiOiJBbGljZSIsImxhc3RfbmFtZSI6IkRvZSIsInRlYW0iOjEsInJlbmRlcmluZ19pZCI6IjQyIiwiZXhwIjoxNjA4MDQ5MTI2fQ.5xaMxjUjE3wKldBsj3wW0BP9GHnnMqQi2Kpde8cIHEw',
87
+ path: '/',
88
+ expires: Time.now.to_i + 14.days,
89
+ secure: true,
90
+ httponly: true
91
+ }
92
+ post ForestLiana::Engine.routes.url_helpers.authentication_logout_path, params: { :renderingId => 42 }, :headers => headers
93
+ cookies.delete('forest_session_token')
94
+ end
95
+
96
+ it "should respond with a 204 code" do
97
+ expect(response).to have_http_status(204)
98
+ end
99
+
100
+ it "should invalidate token from browser" do
101
+ invalidated_session_cookie = response.headers['set-cookie']
102
+ expect(invalidated_session_cookie).to match(/^forest_session_token=[^;]+; path=\/; expires=Thu, 01 Jan 1970 00:00:00 GMT; secure; HttpOnly$/)
103
+ end
104
+ end
105
+ end
@@ -46,12 +46,12 @@ describe 'Requesting Tree resources', :type => :request do
46
46
  }
47
47
 
48
48
  it 'should respond 200' do
49
- get '/forest/Tree', params, headers
49
+ get '/forest/Tree', params: params, headers: headers
50
50
  expect(response.status).to eq(200)
51
51
  end
52
52
 
53
53
  it 'should respond the tree data' do
54
- get '/forest/Tree', params, headers
54
+ get '/forest/Tree', params: params, headers: headers
55
55
  expect(JSON.parse(response.body)).to eq({
56
56
  "data" => [{
57
57
  "type" => "Tree",
@@ -83,12 +83,12 @@ describe 'Requesting Tree resources', :type => :request do
83
83
  }
84
84
 
85
85
  it 'should respond 200' do
86
- get '/forest/Tree', params, headers
86
+ get '/forest/Tree', params: params, headers: headers
87
87
  expect(response.status).to eq(200)
88
88
  end
89
89
 
90
90
  it 'should respond the tree data' do
91
- get '/forest/Tree', params, headers
91
+ get '/forest/Tree', params: params, headers: headers
92
92
  expect(JSON.parse(response.body)).to eq({
93
93
  "data" => [{
94
94
  "type" => "Tree",
@@ -0,0 +1,53 @@
1
+ require 'rails_helper'
2
+ require 'openid_connect'
3
+ require 'json'
4
+
5
+ RSpec.describe "Authentications", type: :request do
6
+ before() do
7
+ allow(ForestLiana::IpWhitelist).to receive(:retrieve) { true }
8
+ allow(ForestLiana::IpWhitelist).to receive(:is_ip_whitelist_retrieved) { true }
9
+ allow(ForestLiana::IpWhitelist).to receive(:is_ip_valid) { true }
10
+
11
+ body = '{"data":{"id":"654","type":"users","attributes":{"email":"user@email.com","first_name":"FirstName","last_name":"LastName","teams":["Operations"]}},"relationships":{"renderings":{"data":[{"id":1,"type":"renderings"}]}}}'
12
+ allow(ForestLiana::ForestApiRequester).to receive(:get).with(
13
+ "/liana/v2/renderings/42/authorization", { :headers => { "forest-token" => "google-access-token" }, :query=> {} }
14
+ ).and_return(
15
+ instance_double(HTTParty::Response, :body => body, :code => 200)
16
+ )
17
+ end
18
+
19
+ after() do
20
+ Rails.cache.delete(URI.join(ForestLiana.application_url, ForestLiana::Engine.routes.url_helpers.authentication_callback_path).to_s)
21
+ end
22
+
23
+ describe "POST /forest/sessions-google" do
24
+ before() do
25
+ post ForestLiana::Engine.routes.url_helpers.sessions_google_path, params: '{ "renderingId": "42", "forestToken": "google-access-token" }', headers: {
26
+ 'Accept' => 'application/json',
27
+ 'Content-Type' => 'application/json',
28
+ }
29
+ end
30
+
31
+ it "should respond with a 200 code" do
32
+ expect(response).to have_http_status(200)
33
+ end
34
+
35
+ it "should return a valid authentication token" do
36
+ response_body = JSON.parse(response.body, :symbolize_names => true)
37
+ expect(response_body).to have_key(:token)
38
+
39
+ token = response_body[:token]
40
+ decoded = JWT.decode(token, ForestLiana.auth_secret, true, { algorithm: 'HS256' })[0]
41
+
42
+ expected_token_data = {
43
+ "id" => '654',
44
+ "email" => 'user@email.com',
45
+ "first_name" => 'FirstName',
46
+ "last_name" => 'LastName',
47
+ "rendering_id" => "42",
48
+ "team" => 'Operations'
49
+ }
50
+ expect(decoded).to include(expected_token_data);
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,711 @@
1
+ module ForestLiana
2
+ describe PermissionsChecker do
3
+ before(:each) do
4
+ described_class.empty_cache
5
+ end
6
+
7
+ let(:user_id) { 1 }
8
+ let(:schema) {
9
+ [
10
+ ForestLiana::Model::Collection.new({
11
+ name: 'all_rights_collection',
12
+ fields: [],
13
+ actions: [
14
+ ForestLiana::Model::Action.new({
15
+ name: 'Test',
16
+ endpoint: 'forest/actions/Test',
17
+ http_method: 'POST'
18
+ }), ForestLiana::Model::Action.new({
19
+ name: 'TestPut',
20
+ endpoint: 'forest/actions/Test',
21
+ http_method: 'PUT'
22
+ }), ForestLiana::Model::Action.new({
23
+ name: 'TestRestricted',
24
+ endpoint: 'forest/actions/TestRestricted',
25
+ http_method: 'POST'
26
+ }), ForestLiana::Model::Action.new({
27
+ name: 'Test Default Values',
28
+ })
29
+ ]
30
+ }), ForestLiana::Model::Collection.new({
31
+ name: 'no_rights_collection',
32
+ fields: [],
33
+ actions: [
34
+ ForestLiana::Model::Action.new({
35
+ name: 'Test',
36
+ endpoint: 'forest/actions/Test',
37
+ http_method: 'POST'
38
+ })
39
+ ]
40
+ }), ForestLiana::Model::Collection.new({
41
+ name: 'custom',
42
+ fields: [],
43
+ actions: []
44
+ })
45
+ ]
46
+ }
47
+ let(:default_api_permissions) {
48
+ {
49
+ "data" => {
50
+ "all_rights_collection" => {
51
+ "collection" => {
52
+ "list" => true,
53
+ "show" => true,
54
+ "create" => true,
55
+ "update" => true,
56
+ "delete" => true,
57
+ "export" => true,
58
+ "searchToEdit" => true
59
+ },
60
+ "actions" => {
61
+ "Test" => {
62
+ "allowed" => true,
63
+ "users" => nil
64
+ },
65
+ "TestPut" => {
66
+ "allowed" => false,
67
+ "users" => nil
68
+ },
69
+ "TestRestricted" => {
70
+ "allowed" => true,
71
+ "users" => [1]
72
+ },
73
+ "Test Default Values" => {
74
+ "allowed" => true,
75
+ "users" => nil
76
+ },
77
+ },
78
+ "scope" => nil
79
+ },
80
+ "no_rights_collection" => {
81
+ "collection" => {
82
+ "list" => false,
83
+ "show" => false,
84
+ "create" => false,
85
+ "update" => false,
86
+ "delete" => false,
87
+ "export" => false,
88
+ "searchToEdit" => false
89
+ },
90
+ "actions" => {
91
+ "Test" => {
92
+ "allowed" => false,
93
+ "users" => nil
94
+ }
95
+ },
96
+ "scope" => nil
97
+ },
98
+ },
99
+ "meta" => {
100
+ "rolesACLActivated" => false
101
+ }
102
+ }
103
+ }
104
+
105
+ before do
106
+ allow(ForestLiana).to receive(:name_for).and_return(collection_name)
107
+ allow(ForestLiana).to receive(:apimap).and_return(schema)
108
+ end
109
+
110
+ describe 'handling cache' do
111
+ let(:collection_name) { 'all_rights_collection' }
112
+ let(:fake_ressource) { nil }
113
+ let(:default_rendering_id) { 1 }
114
+
115
+ context 'when calling twice the same permissions' do
116
+ before do
117
+ # clones is called to duplicate the returned value and not use to same (which results in an error
118
+ # as the permissions is edited through the formatter)
119
+ allow(ForestLiana::PermissionsGetter).to receive(:get_permissions_for_rendering) { default_api_permissions.clone }
120
+ end
121
+
122
+ context 'after expiration time' do
123
+ before do
124
+ allow(ENV).to receive(:[]).with('FOREST_PERMISSIONS_EXPIRATION_IN_SECONDS').and_return('-1')
125
+ # Needed to enforce ENV stub
126
+ described_class.empty_cache
127
+ end
128
+
129
+ it 'should call the API twice' do
130
+ described_class.new(fake_ressource, 'exportEnabled', default_rendering_id, user_id: user_id).is_authorized?
131
+ described_class.new(fake_ressource, 'exportEnabled', default_rendering_id, user_id: user_id).is_authorized?
132
+
133
+ expect(ForestLiana::PermissionsGetter).to have_received(:get_permissions_for_rendering).twice
134
+ end
135
+ end
136
+
137
+ context 'before expiration time' do
138
+ it 'should call the API only once' do
139
+ described_class.new(fake_ressource, 'exportEnabled', default_rendering_id, user_id: user_id).is_authorized?
140
+ described_class.new(fake_ressource, 'exportEnabled', default_rendering_id, user_id: user_id).is_authorized?
141
+
142
+ expect(ForestLiana::PermissionsGetter).to have_received(:get_permissions_for_rendering).once
143
+ end
144
+ end
145
+ end
146
+
147
+ context 'with permissions coming from 2 different renderings' do
148
+ let(:collection_name) { 'custom' }
149
+ let(:api_permissions_rendering_1) {
150
+ {
151
+ "data" => {
152
+ "custom" => {
153
+ "collection" => {
154
+ "list" => true,
155
+ "show" => true,
156
+ "create" => true,
157
+ "update" => true,
158
+ "delete" => true,
159
+ "export" => true,
160
+ "searchToEdit" => true
161
+ },
162
+ "actions" => { },
163
+ "scope" => nil
164
+ },
165
+ },
166
+ "meta" => {
167
+ "rolesACLActivated" => false
168
+ }
169
+ }
170
+ }
171
+ let(:api_permissions_rendering_2) {
172
+ api_permissions_rendering_2 = api_permissions_rendering_1.deep_dup
173
+ api_permissions_rendering_2['data']['custom']['collection']['export'] = false
174
+ api_permissions_rendering_2
175
+ }
176
+ let(:authorized_to_export_rendering_1) { described_class.new(fake_ressource, 'exportEnabled', 1, user_id: user_id).is_authorized? }
177
+ let(:authorized_to_export_rendering_2) { described_class.new(fake_ressource, 'exportEnabled', 2, user_id: user_id).is_authorized? }
178
+
179
+ before do
180
+ allow(ForestLiana::PermissionsGetter).to receive(:get_permissions_for_rendering)
181
+ allow(ForestLiana::PermissionsGetter).to receive(:get_permissions_for_rendering).with(1).and_return(api_permissions_rendering_1)
182
+ allow(ForestLiana::PermissionsGetter).to receive(:get_permissions_for_rendering).with(2).and_return(api_permissions_rendering_2)
183
+ end
184
+
185
+ it 'should return 2 different values' do
186
+ expect(authorized_to_export_rendering_1).to eq true
187
+ expect(authorized_to_export_rendering_2).to eq false
188
+ end
189
+ end
190
+ end
191
+
192
+
193
+ context 'scopes cache' do
194
+ let(:fake_ressource) { nil }
195
+ let(:rendering_id) { 1 }
196
+ let(:collection_name) { 'custom' }
197
+ let(:scope_permissions) { { rendering_id => { 'custom' => nil } } }
198
+ let(:api_permissions) {
199
+ {
200
+ "data" => {
201
+ "custom" => {
202
+ "collection" => {
203
+ "list" => true,
204
+ "show" => true,
205
+ "create" => true,
206
+ "update" => true,
207
+ "delete" => true,
208
+ "export" => true,
209
+ "searchToEdit" => true
210
+ },
211
+ "actions" => { },
212
+ "scope" => nil
213
+ },
214
+ },
215
+ "meta" => {
216
+ "rolesACLActivated" => false
217
+ }
218
+ }
219
+ }
220
+ let(:api_permissions_scope_only) {
221
+ {
222
+ "data" => {
223
+ 'collections' => { },
224
+ 'renderings' => scope_permissions
225
+ },
226
+ "meta" => {
227
+ "rolesACLActivated" => false
228
+ }
229
+ }
230
+ }
231
+
232
+ before do
233
+ # clones is called to duplicate the returned value and not use to same (which results in an error
234
+ # as the permissions is edited through the formatter)
235
+ allow(ForestLiana::PermissionsGetter).to receive(:get_permissions_for_rendering).with(rendering_id) { api_permissions.clone }
236
+ allow(ForestLiana::PermissionsGetter).to receive(:get_permissions_for_rendering).with(rendering_id, rendering_specific_only: true).and_return(api_permissions_scope_only)
237
+ end
238
+
239
+ context 'when checking once for authorization' do
240
+ context 'when checking browseEnabled' do
241
+ context 'when expiration value is set to its default' do
242
+ it 'should not call the API to refresh the scopes cache' do
243
+ described_class.new(fake_ressource, 'browseEnabled', rendering_id, user_id: user_id).is_authorized?
244
+
245
+ expect(ForestLiana::PermissionsGetter).to have_received(:get_permissions_for_rendering).with(rendering_id).once
246
+ expect(ForestLiana::PermissionsGetter).not_to have_received(:get_permissions_for_rendering).with(rendering_id, rendering_specific_only: true)
247
+ end
248
+ end
249
+
250
+ context 'when expiration value is set in the past' do
251
+ before do
252
+ allow(ENV).to receive(:[]).with('FOREST_PERMISSIONS_EXPIRATION_IN_SECONDS').and_return('-1')
253
+ # Needed to enforce ENV stub
254
+ described_class.empty_cache
255
+ end
256
+
257
+ it 'should call the API to refresh the scopes cache' do
258
+ described_class.new(fake_ressource, 'browseEnabled', rendering_id, user_id: user_id).is_authorized?
259
+
260
+ expect(ForestLiana::PermissionsGetter).to have_received(:get_permissions_for_rendering).with(rendering_id).once
261
+ expect(ForestLiana::PermissionsGetter).to have_received(:get_permissions_for_rendering).with(rendering_id, rendering_specific_only: true).once
262
+ end
263
+ end
264
+ end
265
+
266
+ # Only browse permission requires scopes
267
+ context 'when checking exportEnabled' do
268
+ context 'when expiration value is set in the past' do
269
+ before do
270
+ allow(ENV).to receive(:[]).with('FOREST_PERMISSIONS_EXPIRATION_IN_SECONDS').and_return('-1')
271
+ # Needed to enforce ENV stub
272
+ described_class.empty_cache
273
+ end
274
+ end
275
+
276
+ it 'should NOT call the API to refresh the scopes cache' do
277
+ described_class.new(fake_ressource, 'exportEnabled', rendering_id, user_id: user_id).is_authorized?
278
+
279
+ expect(ForestLiana::PermissionsGetter).to have_received(:get_permissions_for_rendering).with(rendering_id).once
280
+ expect(ForestLiana::PermissionsGetter).not_to have_received(:get_permissions_for_rendering).with(rendering_id, rendering_specific_only: true)
281
+ end
282
+ end
283
+ end
284
+
285
+ context 'when checking twice for authorization' do
286
+ context 'on the same rendering' do
287
+ context 'when scopes permission has NOT expired' do
288
+ it 'should NOT call the API to refresh the scopes permissions' do
289
+ described_class.new(fake_ressource, 'browseEnabled', rendering_id, user_id: user_id).is_authorized?
290
+ described_class.new(fake_ressource, 'browseEnabled', rendering_id, user_id: user_id).is_authorized?
291
+
292
+ expect(ForestLiana::PermissionsGetter).to have_received(:get_permissions_for_rendering).with(rendering_id).once
293
+ expect(ForestLiana::PermissionsGetter).not_to have_received(:get_permissions_for_rendering).with(rendering_id, rendering_specific_only: true)
294
+ end
295
+ end
296
+
297
+ context 'when scopes permission has expired' do
298
+ before do
299
+ allow(ENV).to receive(:[]).with('FOREST_PERMISSIONS_EXPIRATION_IN_SECONDS').and_return('-1')
300
+ # Needed to enforce ENV stub
301
+ described_class.empty_cache
302
+ end
303
+
304
+ it 'should call the API to refresh the scopes permissions' do
305
+ described_class.new(fake_ressource, 'browseEnabled', rendering_id, user_id: user_id).is_authorized?
306
+ described_class.new(fake_ressource, 'browseEnabled', rendering_id, user_id: user_id).is_authorized?
307
+
308
+ expect(ForestLiana::PermissionsGetter).to have_received(:get_permissions_for_rendering).with(rendering_id).twice
309
+ expect(ForestLiana::PermissionsGetter).to have_received(:get_permissions_for_rendering).with(rendering_id, rendering_specific_only: true).twice
310
+ end
311
+ end
312
+ end
313
+
314
+ context 'on two different renderings' do
315
+ let(:other_rendering_id) { 2 }
316
+ let(:api_permissions_scope_only) {
317
+ {
318
+ "data" => {
319
+ 'collections' => { },
320
+ 'renderings' => {
321
+ '2' => { 'custom' => nil }
322
+ }
323
+ },
324
+ "meta" => {
325
+ "rolesACLActivated" => false
326
+ }
327
+ }
328
+ }
329
+ let(:api_permissions_copy) { api_permissions.clone }
330
+
331
+ before do
332
+ allow(ForestLiana::PermissionsGetter).to receive(:get_permissions_for_rendering).with(other_rendering_id).and_return(api_permissions_copy)
333
+ allow(ForestLiana::PermissionsGetter).to receive(:get_permissions_for_rendering).with(other_rendering_id, rendering_specific_only: true).and_return(api_permissions_scope_only)
334
+ end
335
+
336
+ it 'should not call the API to refresh the scopes permissions' do
337
+ described_class.new(fake_ressource, 'browseEnabled', rendering_id, user_id: user_id).is_authorized?
338
+ described_class.new(fake_ressource, 'browseEnabled', other_rendering_id, user_id: user_id).is_authorized?
339
+
340
+ expect(ForestLiana::PermissionsGetter).to have_received(:get_permissions_for_rendering).with(rendering_id).once
341
+ expect(ForestLiana::PermissionsGetter).to have_received(:get_permissions_for_rendering).with(other_rendering_id).once
342
+ expect(ForestLiana::PermissionsGetter).not_to have_received(:get_permissions_for_rendering).with(rendering_id, rendering_specific_only: true)
343
+ expect(ForestLiana::PermissionsGetter).not_to have_received(:get_permissions_for_rendering).with(other_rendering_id, rendering_specific_only: true)
344
+ end
345
+ end
346
+ end
347
+ end
348
+
349
+ describe '#is_authorized?' do
350
+ # Resource is only used to retrieve the collection name as it's stubbed it does not
351
+ # need to be defined
352
+ let(:fake_ressource) { nil }
353
+ let(:default_rendering_id) { nil }
354
+ let(:api_permissions) { default_api_permissions }
355
+ let(:collection_name) { 'all_rights_collection' }
356
+
357
+ before do
358
+ allow(ForestLiana::PermissionsGetter).to receive(:get_permissions_for_rendering).and_return(api_permissions)
359
+ end
360
+
361
+ context 'when permissions does NOT have rolesACLActivated' do
362
+ describe 'exportEnabled permission' do
363
+ subject { described_class.new(fake_ressource, 'exportEnabled', default_rendering_id, user_id: user_id) }
364
+
365
+ context 'when user has the required permission' do
366
+ it 'should be authorized' do
367
+ expect(subject.is_authorized?).to be true
368
+ end
369
+ end
370
+
371
+ context 'when user has not the required permission' do
372
+ let(:collection_name) { 'no_rights_collection' }
373
+
374
+ it 'should NOT be authorized' do
375
+ expect(subject.is_authorized?).to be false
376
+ end
377
+ end
378
+ end
379
+
380
+ describe 'browseEnabled permission' do
381
+ let(:collection_name) { 'custom' }
382
+ subject { described_class.new(fake_ressource, 'browseEnabled', default_rendering_id, user_id: user_id) }
383
+ let(:scope_permissions) { nil }
384
+ let(:default_api_permissions) {
385
+ {
386
+ "data" => {
387
+ "custom" => {
388
+ "collection" => collection_permissions,
389
+ "actions" => { },
390
+ "scope" => scope_permissions
391
+ },
392
+ },
393
+ "meta" => {
394
+ "rolesACLActivated" => false
395
+ }
396
+ }
397
+ }
398
+
399
+ context 'when user has list permission' do
400
+ let(:collection_permissions) {
401
+ {
402
+ "list" => true,
403
+ "show" => false,
404
+ "create" => false,
405
+ "update" => false,
406
+ "delete" => false,
407
+ "export" => false,
408
+ "searchToEdit" => false
409
+ }
410
+ }
411
+
412
+ it 'should be authorized' do
413
+ expect(subject.is_authorized?).to be true
414
+ end
415
+ end
416
+
417
+ context 'when user has searchToEdit permission' do
418
+ let(:collection_permissions) {
419
+ {
420
+ "list" => false,
421
+ "show" => false,
422
+ "create" => false,
423
+ "update" => false,
424
+ "delete" => false,
425
+ "export" => false,
426
+ "searchToEdit" => true
427
+ }
428
+ }
429
+
430
+ it 'should be authorized' do
431
+ expect(subject.is_authorized?).to be true
432
+ end
433
+ end
434
+
435
+ context 'when user has not the list nor the searchToEdit permission' do
436
+ let(:collection_permissions) {
437
+ {
438
+ "list" => false,
439
+ "show" => false,
440
+ "create" => false,
441
+ "update" => false,
442
+ "delete" => false,
443
+ "export" => false,
444
+ "searchToEdit" => false
445
+ }
446
+ }
447
+
448
+ it 'should be NOT authorized' do
449
+ expect(subject.is_authorized?).to be false
450
+ end
451
+ end
452
+
453
+ context 'when providing collection_list_parameters' do
454
+ let(:collection_permissions) {
455
+ {
456
+ "list" => true,
457
+ "show" => false,
458
+ "create" => false,
459
+ "update" => false,
460
+ "delete" => false,
461
+ "export" => false,
462
+ "searchToEdit" => false
463
+ }
464
+ }
465
+ let(:collection_list_parameters) { { :user_id => "1", :filters => nil } }
466
+
467
+ subject {
468
+ described_class.new(
469
+ fake_ressource,
470
+ 'browseEnabled',
471
+ default_rendering_id,
472
+ user_id: user_id,
473
+ collection_list_parameters: collection_list_parameters
474
+ )
475
+ }
476
+
477
+ context 'when user has the required permission' do
478
+ it 'should be authorized' do
479
+ expect(subject.is_authorized?).to be true
480
+ end
481
+ end
482
+
483
+ context 'when user has not the required permission' do
484
+ let(:collection_permissions) {
485
+ {
486
+ "list" => false,
487
+ "show" => false,
488
+ "create" => false,
489
+ "update" => false,
490
+ "delete" => false,
491
+ "export" => false,
492
+ "searchToEdit" => false
493
+ }
494
+ }
495
+
496
+ it 'should NOT be authorized' do
497
+ expect(subject.is_authorized?).to be false
498
+ end
499
+ end
500
+
501
+ context 'when scopes are defined' do
502
+ let(:scope_permissions) { { 'dynamicScopesValues' => {}, 'filter' => { 'aggregator' => 'and', 'conditions' => [condition] } }}
503
+ let(:collection_list_parameters) { { :user_id => "1", :filters => JSON.generate(condition) } }
504
+
505
+ context 'when scopes are passing validation' do
506
+ context 'when scope value is a string' do
507
+ let(:condition) { { 'field' => 'field_1', 'operator' => 'equal', 'value' => true } }
508
+
509
+ it 'should return true' do
510
+ expect(subject.is_authorized?).to be true
511
+ end
512
+ end
513
+
514
+ context 'when scope value is a boolean' do
515
+ let(:condition) { { 'field' => 'field_1', 'operator' => 'equal', 'value' => 'true' } }
516
+
517
+ it 'should return true' do
518
+ expect(subject.is_authorized?).to be true
519
+ end
520
+ end
521
+ end
522
+
523
+ context 'when scopes are NOT passing validation' do
524
+ let(:condition) { { 'field' => 'field_1', 'operator' => 'equal', 'value' => true } }
525
+ let(:other_condition) {
526
+ {
527
+ aggregator: 'and',
528
+ conditions: [
529
+ { field: 'name', value: 'john', operator: 'equal' },
530
+ { field: 'price', value: '2500', operator: 'equal' }
531
+ ]
532
+ }
533
+ }
534
+ let(:collection_list_parameters) {
535
+ {
536
+ :user_id => "1",
537
+ :filters => JSON.generate(other_condition)
538
+ }
539
+ }
540
+
541
+
542
+ it 'should return false' do
543
+ expect(subject.is_authorized?).to be false
544
+ end
545
+ end
546
+ end
547
+ end
548
+ end
549
+
550
+ describe 'readEnabled permission' do
551
+ subject { described_class.new(fake_ressource, 'readEnabled', default_rendering_id, user_id: user_id) }
552
+
553
+ context 'when user has the required permission' do
554
+ it 'should be authorized' do
555
+ expect(subject.is_authorized?).to be true
556
+ end
557
+ end
558
+
559
+ context 'when user has not the required permission' do
560
+ let(:collection_name) { 'no_rights_collection' }
561
+
562
+ it 'should NOT be authorized' do
563
+ expect(subject.is_authorized?).to be false
564
+ end
565
+ end
566
+ end
567
+
568
+ describe 'addEnabled permission' do
569
+ subject { described_class.new(fake_ressource, 'addEnabled', default_rendering_id, user_id: user_id) }
570
+
571
+ context 'when user has the required permission' do
572
+ it 'should be authorized' do
573
+ expect(subject.is_authorized?).to be true
574
+ end
575
+ end
576
+
577
+ context 'when user has not the required permission' do
578
+ let(:collection_name) { 'no_rights_collection' }
579
+
580
+ it 'should NOT be authorized' do
581
+ expect(subject.is_authorized?).to be false
582
+ end
583
+ end
584
+ end
585
+
586
+ describe 'editEnabled permission' do
587
+ subject { described_class.new(fake_ressource, 'editEnabled', default_rendering_id, user_id: user_id) }
588
+
589
+ context 'when user has the required permission' do
590
+ it 'should be authorized' do
591
+ expect(subject.is_authorized?).to be true
592
+ end
593
+ end
594
+
595
+ context 'when user has not the required permission' do
596
+ let(:collection_name) { 'no_rights_collection' }
597
+
598
+ it 'should NOT be authorized' do
599
+ expect(subject.is_authorized?).to be false
600
+ end
601
+ end
602
+ end
603
+
604
+ describe 'deleteEnabled permission' do
605
+ subject { described_class.new(fake_ressource, 'deleteEnabled', default_rendering_id, user_id: user_id) }
606
+
607
+ context 'when user has the required permission' do
608
+ it 'should be authorized' do
609
+ expect(subject.is_authorized?).to be true
610
+ end
611
+ end
612
+
613
+ context 'when user has not the required permission' do
614
+ let(:collection_name) { 'no_rights_collection' }
615
+
616
+ it 'should NOT be authorized' do
617
+ expect(subject.is_authorized?).to be false
618
+ end
619
+ end
620
+ end
621
+
622
+ describe 'actions permission' do
623
+ let(:smart_action_request_info) { { endpoint: 'forest/actions/Test', http_method: 'POST' } }
624
+ subject {
625
+ described_class.new(
626
+ fake_ressource,
627
+ 'actions',
628
+ default_rendering_id,
629
+ user_id: user_id,
630
+ smart_action_request_info: smart_action_request_info
631
+ )
632
+ }
633
+
634
+ context 'when user has the required permission' do
635
+
636
+ it 'should be authorized' do
637
+ expect(subject.is_authorized?).to be true
638
+ end
639
+ end
640
+
641
+ context 'when user has not the required permission' do
642
+ let(:collection_name) { 'no_rights_collection' }
643
+
644
+ it 'should NOT be authorized' do
645
+ expect(subject.is_authorized?).to be false
646
+ end
647
+ end
648
+
649
+ context 'when endpoint is missing from smart action parameters' do
650
+ let(:smart_action_request_info) { { http_method: 'POST' } }
651
+
652
+ it 'user should NOT be authorized' do
653
+ expect(subject.is_authorized?).to be false
654
+ end
655
+ end
656
+
657
+ context 'when http_method is missing from smart action parameters' do
658
+ let(:smart_action_request_info) { { endpoint: 'forest/actions/Test' } }
659
+
660
+ it 'user should NOT be authorized' do
661
+ expect(subject.is_authorized?).to be false
662
+ end
663
+ end
664
+
665
+ context 'when the provided endpoint is not part of the schema' do
666
+ let(:smart_action_request_info) { { endpoint: 'forest/actions/Test', http_method: 'DELETE' } }
667
+
668
+ it 'user should NOT be authorized' do
669
+ expect(subject.is_authorized?).to be false
670
+ end
671
+ end
672
+
673
+ context 'when the action permissions contains a list of user ids' do
674
+ context 'when user id is NOT part of the authorized users' do
675
+ let(:user_id) { 2 }
676
+ let(:smart_action_request_info) { { endpoint: 'forest/actions/TestRestricted', http_method: 'POST' } }
677
+
678
+ it 'user should NOT be authorized' do
679
+ expect(subject.is_authorized?).to be false
680
+ end
681
+ end
682
+
683
+ context 'when user id is part of the authorized users' do
684
+ let(:smart_action_request_info) { { endpoint: 'forest/actions/TestRestricted', http_method: 'POST' } }
685
+
686
+ it 'user should be authorized' do
687
+ expect(subject.is_authorized?).to be true
688
+ end
689
+ end
690
+ end
691
+
692
+ context 'when the action has been created with default http endpoint and method in the schema' do
693
+ let(:smart_action_request_info) { { endpoint: 'forest/actions/test-default-values', http_method: 'POST' } }
694
+
695
+ it 'user should be authorized' do
696
+ expect(subject.is_authorized?).to be true
697
+ end
698
+ end
699
+
700
+ context 'when the action has the same enpoint as an other' do
701
+ let(:smart_action_request_info) { { endpoint: 'forest/actions/Test', http_method: 'PUT' } }
702
+
703
+ it 'user should NOT be authorized' do
704
+ expect(subject.is_authorized?).to be false
705
+ end
706
+ end
707
+ end
708
+ end
709
+ end
710
+ end
711
+ end