forest_liana 5.3.0 → 6.0.0.pre.beta.2

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 (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)}"