forest_liana 5.3.0 → 6.0.0.pre.beta.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/forest_liana/actions_controller.rb +0 -82
  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/router.rb +2 -2
  7. data/app/controllers/forest_liana/sessions_controller.rb +1 -1
  8. data/app/controllers/forest_liana/stats_controller.rb +5 -5
  9. data/app/helpers/forest_liana/adapter_helper.rb +1 -1
  10. data/app/models/forest_liana/model/action.rb +1 -2
  11. data/app/serializers/forest_liana/schema_serializer.rb +2 -2
  12. data/app/services/forest_liana/apimap_sorter.rb +1 -2
  13. data/app/services/forest_liana/authentication.rb +59 -0
  14. data/app/services/forest_liana/authorization_getter.rb +12 -20
  15. data/app/services/forest_liana/forest_api_requester.rb +14 -5
  16. data/app/services/forest_liana/ip_whitelist_checker.rb +1 -1
  17. data/app/services/forest_liana/login_handler.rb +3 -11
  18. data/app/services/forest_liana/oidc_client_manager.rb +34 -0
  19. data/app/services/forest_liana/oidc_configuration_retriever.rb +12 -0
  20. data/app/services/forest_liana/oidc_dynamic_client_registrator.rb +67 -0
  21. data/app/services/forest_liana/permissions_checker.rb +1 -1
  22. data/app/services/forest_liana/query_stat_getter.rb +5 -5
  23. data/app/services/forest_liana/resources_getter.rb +3 -3
  24. data/app/services/forest_liana/token.rb +27 -0
  25. data/config/initializers/error-messages.rb +20 -0
  26. data/config/routes.rb +5 -2
  27. data/lib/forest_liana.rb +1 -0
  28. data/lib/forest_liana/bootstrapper.rb +1 -20
  29. data/lib/forest_liana/collection.rb +2 -2
  30. data/lib/forest_liana/engine.rb +9 -0
  31. data/lib/forest_liana/json_printer.rb +1 -1
  32. data/lib/forest_liana/schema_file_updater.rb +0 -1
  33. data/lib/forest_liana/version.rb +1 -1
  34. data/spec/dummy/config/initializers/forest_liana.rb +1 -0
  35. data/spec/requests/authentications_spec.rb +107 -0
  36. data/spec/requests/sessions_spec.rb +55 -0
  37. data/spec/services/forest_liana/apimap_sorter_spec.rb +4 -6
  38. metadata +57 -9
  39. data/app/helpers/forest_liana/is_same_data_structure_helper.rb +0 -44
  40. data/spec/helpers/forest_liana/is_same_data_structure_helper_spec.rb +0 -87
  41. data/spec/requests/actions_controller_spec.rb +0 -136
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e035848d2e7346973187338d78579301dcf062b2b6192ba383cd1f50dcccf584
4
- data.tar.gz: 5f8a3ecdea7a8f4643922c27626397bfa3d497c14302f77f377153a22f064b0f
3
+ metadata.gz: f2f88ccbd15c18a78b06d08ad2a17dfa2075d2b586728033cbe03564c95dd92e
4
+ data.tar.gz: e78f4892dc9ea07b8dbabb67d3183920f8ef41b6cb7ebe1edd8cb3d0c3b7ee2b
5
5
  SHA512:
6
- metadata.gz: a30b760625155ae65acf9c8d6941e1e8ddd408b98cd377fdd6f6385c943ceef74e72cf3318d1373f966f8eda48dd9c35c4198bdaeecba2c9c6ffd52f2285e465
7
- data.tar.gz: 52b468bfea30b0c115c318ab60eefd2caf382c94df171ea53ec0e203d6869d823f0deb23b20f66939f2560a8611d55b2c6b64f1dcb0b5ffb3d461de3471db8a5
6
+ metadata.gz: 2a1dfc618f2723448b2fea3b39a48be64fad619c87a3b7d9257e3be8292f1ae86632c38563a7c9d938fd9699a0d78bac50e521d390bb40077ab824996b987ed2
7
+ data.tar.gz: d1a6401e8e236b718f488e1a1b5641fb953a912c719a5ea1010f7f307c99e7b96fd9f153fc483ebd7dbef5861a5daf9c12e2bdc03f846b62850662ee1837209a
@@ -1,89 +1,7 @@
1
1
  module ForestLiana
2
2
  class ActionsController < ForestLiana::BaseController
3
-
4
3
  def values
5
4
  render serializer: nil, json: {}, status: :ok
6
5
  end
7
-
8
- def get_collection(collection_name)
9
- ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name }
10
- end
11
-
12
- def get_action(collection_name)
13
- collection = get_collection(collection_name)
14
- begin
15
- collection.actions.find {|action| ActiveSupport::Inflector.parameterize(action.name) == params[:action_name]}
16
- rescue => error
17
- FOREST_LOGGER.error "Smart Action get action retrieval error: #{error}"
18
- nil
19
- end
20
- end
21
-
22
- def get_record
23
- model = ForestLiana::SchemaUtils.find_model_from_collection_name(params[:collectionName])
24
- redord_getter = ForestLiana::ResourceGetter.new(model, {:id => params[:recordIds][0]})
25
- redord_getter.perform
26
- redord_getter.record
27
- end
28
-
29
- def get_smart_action_load_ctx(fields)
30
- fields = fields.reduce({}) {|p, c| p.update(c[:field] => c.merge!(value: nil))}
31
- {:record => get_record, :fields => fields}
32
- end
33
-
34
- def get_smart_action_change_ctx(fields)
35
- fields = fields.reduce({}) {|p, c| p.update(c[:field] => c.permit!.to_h)}
36
- {:record => get_record, :fields => fields}
37
- end
38
-
39
- def handle_result(result, formatted_fields, action)
40
- if result.nil? || !result.is_a?(Hash)
41
- return render status: 500, json: { error: 'Error in smart action load hook: hook must return an object' }
42
- end
43
- is_same_data_structure = ForestLiana::IsSameDataStructureHelper::Analyser.new(formatted_fields, result, 1)
44
- unless is_same_data_structure.perform
45
- return render status: 500, json: { error: 'Error in smart action hook: fields must be unchanged (no addition nor deletion allowed)' }
46
- end
47
-
48
- # Apply result on fields (transform the object back to an array), preserve order.
49
- fields = action.fields.map { |field| result[field[:field]] }
50
-
51
- render serializer: nil, json: { fields: fields}, status: :ok
52
- end
53
-
54
- def load
55
- action = get_action(params[:collectionName])
56
-
57
- if !action
58
- render status: 500, json: {error: 'Error in smart action load hook: cannot retrieve action from collection'}
59
- else
60
- # Transform fields from array to an object to ease usage in hook, adds null value.
61
- context = get_smart_action_load_ctx(action.fields)
62
- formatted_fields = context[:fields].clone # clone for following test on is_same_data_structure
63
-
64
- # Call the user-defined load hook.
65
- result = action.hooks[:load].(context)
66
-
67
- handle_result(result, formatted_fields, action)
68
- end
69
- end
70
-
71
- def change
72
- action = get_action(params[:collectionName])
73
-
74
- if !action
75
- render status: 500, json: {error: 'Error in smart action change hook: cannot retrieve action from collection'}
76
- else
77
- # Transform fields from array to an object to ease usage in hook.
78
- context = get_smart_action_change_ctx(params[:fields])
79
- formatted_fields = context[:fields].clone # clone for following test on is_same_data_structure
80
-
81
- # Call the user-defined change hook.
82
- field_name = params[:fields].select { |field| field[:value] != field[:previousValue] }[0][:field]
83
- result = action.hooks[:change][field_name].(context)
84
-
85
- handle_result(result, formatted_fields, action)
86
- end
87
- end
88
6
  end
89
7
  end
@@ -3,8 +3,6 @@ require 'csv'
3
3
 
4
4
  module ForestLiana
5
5
  class ApplicationController < ForestLiana::BaseController
6
- REGEX_COOKIE_SESSION_TOKEN = /forest_session_token=([^;]*)/;
7
-
8
6
  def self.papertrail?
9
7
  Object.const_get('PaperTrail::Version').is_a?(Class) rescue false
10
8
  end
@@ -64,7 +62,7 @@ module ForestLiana
64
62
  token = request.headers['Authorization'].split.second
65
63
  # NOTICE: Necessary for downloads authentication.
66
64
  elsif request.headers['cookie']
67
- match = REGEX_COOKIE_SESSION_TOKEN.match(request.headers['cookie'])
65
+ match = ForestLiana::Token::REGEX_COOKIE_SESSION_TOKEN.match(request.headers['cookie'])
68
66
  token = match[1] if match && match[1]
69
67
  end
70
68
 
@@ -97,10 +95,6 @@ module ForestLiana
97
95
  end
98
96
  end
99
97
 
100
- def route_not_found
101
- head :not_found
102
- end
103
-
104
98
  def internal_server_error
105
99
  head :internal_server_error
106
100
  end
@@ -0,0 +1,122 @@
1
+ require 'uri'
2
+ require 'json'
3
+
4
+ module ForestLiana
5
+ class AuthenticationController < ForestLiana::BaseController
6
+ START_AUTHENTICATION_ROUTE = 'authentication'
7
+ CALLBACK_AUTHENTICATION_ROUTE = 'authentication/callback'
8
+ LOGOUT_ROUTE = 'authentication/logout';
9
+ PUBLIC_ROUTES = [
10
+ "/#{START_AUTHENTICATION_ROUTE}",
11
+ "/#{CALLBACK_AUTHENTICATION_ROUTE}",
12
+ "/#{LOGOUT_ROUTE}",
13
+ ]
14
+
15
+ def initialize
16
+ @authentication_service = ForestLiana::Authentication.new()
17
+ end
18
+
19
+ def get_callback_url
20
+ URI.join(ForestLiana.application_url, "/forest/#{CALLBACK_AUTHENTICATION_ROUTE}").to_s
21
+ rescue => error
22
+ raise "application_url is not valid or not defined" if error.is_a?(ArgumentError)
23
+ end
24
+
25
+ def get_and_check_rendering_id
26
+ if !params.has_key?('renderingId')
27
+ raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:MISSING_RENDERING_ID]
28
+ end
29
+
30
+ rendering_id = params[:renderingId]
31
+
32
+ if !(rendering_id.instance_of?(String) || rendering_id.instance_of?(Numeric)) || (rendering_id.instance_of?(Numeric) && rendering_id.nan?)
33
+ raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:INVALID_RENDERING_ID]
34
+ end
35
+
36
+ return rendering_id.to_i
37
+ end
38
+
39
+ def start_authentication
40
+ begin
41
+ rendering_id = get_and_check_rendering_id()
42
+ callback_url = get_callback_url()
43
+
44
+ result = @authentication_service.start_authentication(
45
+ callback_url,
46
+ { 'renderingId' => rendering_id },
47
+ )
48
+
49
+ redirect_to(result['authorization_url'])
50
+ rescue => error
51
+ render json: { errors: [{ status: 500, detail: error.message }] },
52
+ status: :internal_server_error, serializer: nil
53
+ end
54
+ end
55
+
56
+ def authentication_callback
57
+ begin
58
+ callback_url = get_callback_url()
59
+
60
+ token = @authentication_service.verify_code_and_generate_token(
61
+ callback_url,
62
+ params,
63
+ )
64
+
65
+ response.set_cookie(
66
+ 'forest_session_token',
67
+ {
68
+ value: token,
69
+ httponly: true,
70
+ secure: true,
71
+ expires: ForestLiana::Token.expiration_in_days,
72
+ samesite: 'none',
73
+ path: '/'
74
+ },
75
+ )
76
+
77
+ response_body = {
78
+ tokenData: JWT.decode(token, ForestLiana.auth_secret, true, { algorithm: 'HS256' })[0]
79
+ }
80
+
81
+ # The token is sent decoded, because we don't want to share the whole, signed token
82
+ # that is used to authenticate people
83
+ # but the token itself contains interesting values, such as its expiration date
84
+ response_body[:token] = token if !ForestLiana.application_url.start_with?('https://')
85
+
86
+ render json: response_body, status: 200
87
+
88
+ rescue => error
89
+ render json: { errors: [{ status: 500, detail: error.message }] },
90
+ status: :internal_server_error, serializer: nil
91
+ end
92
+ end
93
+
94
+ def logout
95
+ begin
96
+ if cookies.has_key?(:forest_session_token)
97
+ forest_session_token = cookies[:forest_session_token]
98
+
99
+ if forest_session_token
100
+ response.set_cookie(
101
+ 'forest_session_token',
102
+ {
103
+ value: forest_session_token,
104
+ httponly: true,
105
+ secure: true,
106
+ expires: Time.at(0),
107
+ samesite: 'none',
108
+ path: '/'
109
+ },
110
+ )
111
+ end
112
+ end
113
+
114
+ render json: {}, status: 204
115
+ rescue => error
116
+ render json: { errors: [{ status: 500, detail: error.message }] },
117
+ status: :internal_server_error, serializer: nil
118
+ end
119
+ end
120
+
121
+ end
122
+ end
@@ -4,6 +4,10 @@ module ForestLiana
4
4
  wrap_parameters false
5
5
  before_action :reject_unauthorized_ip
6
6
 
7
+ def route_not_found
8
+ head :not_found
9
+ end
10
+
7
11
  private
8
12
 
9
13
  def reject_unauthorized_ip
@@ -7,7 +7,7 @@ class ForestLiana::Router
7
7
  if resource.nil?
8
8
  FOREST_LOGGER.error "Routing error: Resource not found for collection #{collection_name}."
9
9
  FOREST_LOGGER.error "If this is a Smart Collection, please ensure your Smart Collection routes are defined before the mounted ForestLiana::Engine?"
10
- ForestLiana::ApplicationController.action(:route_not_found).call(env)
10
+ ForestLiana::BaseController.action(:route_not_found).call(env)
11
11
  else
12
12
  begin
13
13
  component_prefix = ForestLiana.component_prefix(resource)
@@ -40,7 +40,7 @@ class ForestLiana::Router
40
40
  controller.action(action.to_sym).call(env)
41
41
  rescue NoMethodError => exception
42
42
  FOREST_LOGGER.error "Routing error: #{exception}\n#{exception.backtrace.join("\n\t")}"
43
- ForestLiana::ApplicationController.action(:route_not_found).call(env)
43
+ ForestLiana::BaseController.action(:route_not_found).call(env)
44
44
  end
45
45
  end
46
46
  end
@@ -85,7 +85,7 @@ module ForestLiana
85
85
  # NOTICE: Set a cookie to ensure secure authentication using export feature.
86
86
  # NOTICE: The token is empty at first authentication step if the 2FA option is active.
87
87
  if reponse_data[:token]
88
- response.set_cookie("forest_session_token", { value: reponse_data[:token], expires: (Time.current + 14.days) })
88
+ response.set_cookie("forest_session_token", { value: reponse_data[:token], expires: (ForestLiana::Token.expiration_in_days) })
89
89
  end
90
90
 
91
91
  render(json: reponse_data, serializer: nil)
@@ -6,11 +6,11 @@ module ForestLiana
6
6
  before_action :find_resource, except: [:get_with_live_query]
7
7
  end
8
8
 
9
- CHART_TYPE_VALUE = 'Value';
10
- CHART_TYPE_PIE = 'Pie';
11
- CHART_TYPE_LINE = 'Line';
12
- CHART_TYPE_LEADERBOARD = 'Leaderboard';
13
- CHART_TYPE_OBJECTIVE = 'Objective';
9
+ CHART_TYPE_VALUE = 'Value'
10
+ CHART_TYPE_PIE = 'Pie'
11
+ CHART_TYPE_LINE = 'Line'
12
+ CHART_TYPE_LEADERBOARD = 'Leaderboard'
13
+ CHART_TYPE_OBJECTIVE = 'Objective'
14
14
 
15
15
  def get
16
16
  case params[:type]
@@ -10,7 +10,7 @@ module ForestLiana
10
10
 
11
11
  def self.cast_boolean(value)
12
12
  if ['MySQL', 'SQLite'].include?(ActiveRecord::Base.connection.adapter_name)
13
- value === 'true' ? 1 : 0;
13
+ value === 'true' ? 1 : 0
14
14
  else
15
15
  value
16
16
  end
@@ -5,7 +5,7 @@ class ForestLiana::Model::Action
5
5
  extend ActiveModel::Naming
6
6
 
7
7
  attr_accessor :id, :name, :base_url, :endpoint, :http_method, :fields, :redirect,
8
- :type, :download, :hooks
8
+ :type, :download
9
9
 
10
10
  def initialize(attributes = {})
11
11
  if attributes.key?(:global)
@@ -66,7 +66,6 @@ class ForestLiana::Model::Action
66
66
  @base_url ||= nil
67
67
  @type ||= "bulk"
68
68
  @download ||= false
69
- @hooks = !@hooks.nil? ? @hooks.symbolize_keys : nil
70
69
  end
71
70
 
72
71
  def persisted?
@@ -52,7 +52,7 @@ class ForestLiana::SchemaSerializer
52
52
  @included << format_child_content('segments', segment_id, segment)
53
53
  end
54
54
  else
55
- collection_serialized[:attributes][attribute.to_sym] = value;
55
+ collection_serialized[:attributes][attribute.to_sym] = value
56
56
  end
57
57
  end
58
58
 
@@ -75,7 +75,7 @@ class ForestLiana::SchemaSerializer
75
75
  }
76
76
 
77
77
  object.each do |attribute, value|
78
- child_serialized[:attributes][attribute.to_sym] = value;
78
+ child_serialized[:attributes][attribute.to_sym] = value
79
79
  end
80
80
 
81
81
  child_serialized
@@ -39,7 +39,6 @@ module ForestLiana
39
39
  'redirect',
40
40
  'download',
41
41
  'fields',
42
- 'hooks',
43
42
  ]
44
43
  KEYS_ACTION_FIELD = [
45
44
  'field',
@@ -61,7 +60,7 @@ module ForestLiana
61
60
  def perform
62
61
  begin
63
62
  @apimap = reorder_keys_basic(@apimap)
64
- sort_array_of_objects(@apimap['data']);
63
+ sort_array_of_objects(@apimap['data'])
65
64
  @apimap['data'].map! do |collection|
66
65
  collection = reorder_keys_child(collection)
67
66
  collection['attributes'] = reorder_collection_attributes(collection['attributes'])
@@ -0,0 +1,59 @@
1
+ module ForestLiana
2
+ class Authentication
3
+ def start_authentication(redirect_url, state)
4
+ client = ForestLiana::OidcClientManager.get_client_for_callback_url(redirect_url)
5
+
6
+ authorization_url = client.authorization_uri({
7
+ scope: 'openid email profile',
8
+ state: state.to_s,
9
+ })
10
+
11
+ { 'authorization_url' => authorization_url }
12
+ end
13
+
14
+ def verify_code_and_generate_token(redirect_url, params)
15
+ client = ForestLiana::OidcClientManager.get_client_for_callback_url(redirect_url)
16
+
17
+ rendering_id = parse_state(params['state'])
18
+ client.authorization_code = params['code']
19
+
20
+ if Rails.env.development? || Rails.env.test?
21
+ OpenIDConnect.http_config do |config|
22
+ config.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
23
+ end
24
+ end
25
+ access_token_instance = client.access_token! 'none'
26
+
27
+ user = ForestLiana::AuthorizationGetter.authenticate(
28
+ rendering_id,
29
+ true,
30
+ { :forest_token => access_token_instance.instance_variable_get(:@access_token) },
31
+ nil,
32
+ )
33
+
34
+ return ForestLiana::Token.create_token(user, rendering_id)
35
+ end
36
+
37
+ private
38
+ def parse_state(state)
39
+ unless state
40
+ raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:INVALID_STATE_MISSING]
41
+ end
42
+
43
+ rendering_id = nil
44
+
45
+ begin
46
+ parsed_state = JSON.parse(state.gsub("'",'"').gsub('=>',':'))
47
+ rendering_id = parsed_state["renderingId"].to_s
48
+ rescue
49
+ raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:INVALID_STATE_FORMAT]
50
+ end
51
+
52
+ if rendering_id.nil?
53
+ raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:INVALID_STATE_RENDERING_ID]
54
+ end
55
+
56
+ return rendering_id
57
+ end
58
+ end
59
+ end
@@ -1,39 +1,31 @@
1
1
  module ForestLiana
2
2
  class AuthorizationGetter
3
- def initialize(rendering_id, use_google_authentication, auth_data, two_factor_registration)
4
- @rendering_id = rendering_id
5
- @use_google_authentication = use_google_authentication
6
- @auth_data = auth_data
7
- @two_factor_registration = two_factor_registration
8
-
9
- @route = "/liana/v2/renderings/#{rendering_id}"
10
- @route += use_google_authentication ? "/google-authorization" : "/authorization"
11
- end
12
-
13
- def perform
3
+ def self.authenticate(rendering_id, use_google_authentication, auth_data, two_factor_registration)
14
4
  begin
15
- if @use_google_authentication
16
- headers = { 'forest-token' => @auth_data[:forest_token] }
17
- else
18
- headers = { 'email' => @auth_data[:email], 'password' => @auth_data[:password] }
5
+ route = "/liana/v2/renderings/#{rendering_id.to_s}/authorization"
6
+
7
+ if !use_google_authentication.nil?
8
+ headers = { 'forest-token' => auth_data[:forest_token] }
9
+ elsif !auth_data[:email].nil?
10
+ headers = { 'email' => auth_data[:email], 'password' => auth_data[:password] }
19
11
  end
20
12
 
21
13
  query_parameters = {}
22
14
 
23
- if @two_factor_registration
15
+ unless two_factor_registration.nil?
24
16
  query_parameters['two-factor-registration'] = true
25
17
  end
26
18
 
27
19
  response = ForestLiana::ForestApiRequester
28
- .get(@route, query: query_parameters, headers: headers)
20
+ .get(route, query: query_parameters, headers: headers)
29
21
 
30
- if response.is_a?(Net::HTTPOK)
31
- body = JSON.parse(response.body)
22
+ if response.code.to_i == 200
23
+ body = JSON.parse(response.body, :symbolize_names => false)
32
24
  user = body['data']['attributes']
33
25
  user['id'] = body['data']['id']
34
26
  user
35
27
  else
36
- if @use_google_authentication
28
+ unless use_google_authentication.nil?
37
29
  raise "Cannot authorize the user using this google account. Forest API returned an #{Errors::HTTPErrorHelper.format(response)}"
38
30
  else
39
31
  raise "Cannot authorize the user using this email/password. Forest API returned an #{Errors::HTTPErrorHelper.format(response)}"