forest_liana 6.0.0.pre.beta.1 → 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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/forest_liana/application_controller.rb +1 -7
  3. data/app/controllers/forest_liana/authentication_controller.rb +122 -0
  4. data/app/controllers/forest_liana/base_controller.rb +4 -0
  5. data/app/controllers/forest_liana/router.rb +2 -2
  6. data/app/controllers/forest_liana/sessions_controller.rb +1 -1
  7. data/app/controllers/forest_liana/stats_controller.rb +5 -5
  8. data/app/helpers/forest_liana/adapter_helper.rb +1 -1
  9. data/app/serializers/forest_liana/schema_serializer.rb +2 -2
  10. data/app/services/forest_liana/apimap_sorter.rb +1 -1
  11. data/app/services/forest_liana/authentication.rb +59 -0
  12. data/app/services/forest_liana/authorization_getter.rb +12 -20
  13. data/app/services/forest_liana/forest_api_requester.rb +14 -5
  14. data/app/services/forest_liana/ip_whitelist_checker.rb +1 -1
  15. data/app/services/forest_liana/login_handler.rb +3 -11
  16. data/app/services/forest_liana/oidc_client_manager.rb +34 -0
  17. data/app/services/forest_liana/oidc_configuration_retriever.rb +12 -0
  18. data/app/services/forest_liana/oidc_dynamic_client_registrator.rb +67 -0
  19. data/app/services/forest_liana/permissions_checker.rb +1 -1
  20. data/app/services/forest_liana/query_stat_getter.rb +5 -5
  21. data/app/services/forest_liana/token.rb +27 -0
  22. data/config/initializers/error-messages.rb +20 -0
  23. data/config/routes.rb +5 -0
  24. data/lib/forest_liana.rb +1 -0
  25. data/lib/forest_liana/bootstrapper.rb +1 -1
  26. data/lib/forest_liana/collection.rb +2 -2
  27. data/lib/forest_liana/engine.rb +9 -0
  28. data/lib/forest_liana/json_printer.rb +1 -1
  29. data/lib/forest_liana/version.rb +1 -1
  30. data/spec/dummy/config/initializers/forest_liana.rb +1 -0
  31. data/spec/requests/authentications_spec.rb +107 -0
  32. data/spec/requests/sessions_spec.rb +55 -0
  33. data/spec/services/forest_liana/schema_adapter_spec.rb +1 -1
  34. metadata +54 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a534b6abdb1a47d2690f785ef13dff74d97117fa7e1871b82659644ac9c1a53
4
- data.tar.gz: 9d7c3281edc36a9aabf47f74851c3ba191d1e31c7bd29a0a4c7d9e8b6f3522f7
3
+ metadata.gz: f2f88ccbd15c18a78b06d08ad2a17dfa2075d2b586728033cbe03564c95dd92e
4
+ data.tar.gz: e78f4892dc9ea07b8dbabb67d3183920f8ef41b6cb7ebe1edd8cb3d0c3b7ee2b
5
5
  SHA512:
6
- metadata.gz: c18f213548f7584a4dc7aabe121442d0e50270f29860c54164348bd45fca77810d5402284147b9a114d04f17b5314ee8d7f64daae1887e6f258cfd7663ba6c1f
7
- data.tar.gz: 6edfe33f160b65de926013b45ef0ba4b7ae374a4446faf64ca6ffbd3a5e606845c89c9e30756fe8e26d4d420786ab09a36b38b0077d0c975a4d695b0aaa0c5ef
6
+ metadata.gz: 2a1dfc618f2723448b2fea3b39a48be64fad619c87a3b7d9257e3be8292f1ae86632c38563a7c9d938fd9699a0d78bac50e521d390bb40077ab824996b987ed2
7
+ data.tar.gz: d1a6401e8e236b718f488e1a1b5641fb953a912c719a5ea1010f7f307c99e7b96fd9f153fc483ebd7dbef5861a5daf9c12e2bdc03f846b62850662ee1837209a
@@ -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
@@ -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
@@ -60,7 +60,7 @@ module ForestLiana
60
60
  def perform
61
61
  begin
62
62
  @apimap = reorder_keys_basic(@apimap)
63
- sort_array_of_objects(@apimap['data']);
63
+ sort_array_of_objects(@apimap['data'])
64
64
  @apimap['data'].map! do |collection|
65
65
  collection = reorder_keys_child(collection)
66
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)}"
@@ -5,33 +5,42 @@ module ForestLiana
5
5
  def self.get(route, query: nil, headers: {})
6
6
  begin
7
7
  HTTParty.get("#{forest_api_url}#{route}", {
8
+ :verify => Rails.env.production?,
8
9
  headers: base_headers.merge(headers),
9
10
  query: query,
10
11
  }).response
11
12
  rescue
12
- raise 'Cannot reach Forest API, it seems to be down right now.'
13
+ raise "Cannot reach Forest API at #{forest_api_url}#{route}, it seems to be down right now."
13
14
  end
14
15
  end
15
16
 
16
17
  def self.post(route, body: nil, query: nil, headers: {})
17
18
  begin
18
- HTTParty.post("#{forest_api_url}#{route}", {
19
+ if route.start_with?('https://')
20
+ post_route = route
21
+ else
22
+ post_route = "#{forest_api_url}#{route}"
23
+ end
24
+
25
+ HTTParty.post(post_route, {
26
+ :verify => Rails.env.production?,
19
27
  headers: base_headers.merge(headers),
20
28
  query: query,
21
29
  body: body.to_json,
22
30
  }).response
23
31
  rescue
24
- raise 'Cannot reach Forest API, it seems to be down right now.'
32
+ raise "Cannot reach Forest API at #{post_route}, it seems to be down right now."
25
33
  end
26
34
  end
27
35
 
28
36
  private
29
37
 
30
38
  def self.base_headers
31
- {
39
+ base_headers = {
32
40
  'Content-Type' => 'application/json',
33
- 'forest-secret-key' => ForestLiana.env_secret,
34
41
  }
42
+ base_headers['forest-secret-key'] = ForestLiana.env_secret if !ForestLiana.env_secret.nil?
43
+ return base_headers
35
44
  end
36
45
 
37
46
  def self.forest_api_url
@@ -58,7 +58,7 @@ module ForestLiana
58
58
  ip_range_maximum = (IPAddress rule['ip_maximum']).to_i
59
59
  ip_value = (IPAddress ip).to_i
60
60
 
61
- return ip_value >= ip_range_minimum && ip_value <= ip_range_maximum;
61
+ return ip_value >= ip_range_minimum && ip_value <= ip_range_maximum
62
62
  end
63
63
 
64
64
  def self.is_ip_match_subnet(ip, subnet)
@@ -19,12 +19,12 @@ module ForestLiana
19
19
  end
20
20
 
21
21
  def perform
22
- user = ForestLiana::AuthorizationGetter.new(
22
+ user = ForestLiana::AuthorizationGetter.authenticate(
23
23
  @rendering_id,
24
24
  @use_google_authentication,
25
25
  @auth_data,
26
26
  @two_factor_registration
27
- ).perform
27
+ )
28
28
 
29
29
  if user['two_factor_authentication_enabled']
30
30
  if !@two_factor_token.nil?
@@ -93,15 +93,7 @@ module ForestLiana
93
93
  end
94
94
 
95
95
  def create_token(user, rendering_id)
96
- JWT.encode({
97
- id: user['id'],
98
- email: user['email'],
99
- first_name: user['first_name'],
100
- last_name: user['last_name'],
101
- team: user['teams'][0],
102
- rendering_id: rendering_id,
103
- exp: Time.now.to_i + 2.weeks.to_i
104
- }, ForestLiana.auth_secret, 'HS256')
96
+ ForestLiana::Token.create_token(user, rendering_id)
105
97
  end
106
98
  end
107
99
  end
@@ -0,0 +1,34 @@
1
+ require 'openid_connect'
2
+
3
+ module ForestLiana
4
+ class OidcClientManager
5
+ def self.get_client_for_callback_url(callback_url)
6
+ begin
7
+ client_data = Rails.cache.read(callback_url) || nil
8
+ if client_data.nil?
9
+ configuration = ForestLiana::OidcConfigurationRetriever.retrieve()
10
+
11
+ client_credentials = ForestLiana::OidcDynamicClientRegistrator.register({
12
+ token_endpoint_auth_method: 'none',
13
+ redirect_uris: [callback_url],
14
+ registration_endpoint: configuration['registration_endpoint']
15
+ })
16
+
17
+ client_data = { :client_id => client_credentials['client_id'], :issuer => configuration['issuer'] }
18
+ Rails.cache.write(callback_url, client_data)
19
+ end
20
+
21
+ OpenIDConnect::Client.new(
22
+ identifier: client_data[:client_id],
23
+ redirect_uri: callback_url,
24
+ host: "#{client_data[:issuer].sub(/^https?\:\/\/(www.)?/,'')}",
25
+ authorization_endpoint: '/oidc/auth',
26
+ token_endpoint: '/oidc/token',
27
+ )
28
+ rescue => error
29
+ Rails.cache.delete(callback_url)
30
+ raise error
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,12 @@
1
+ module ForestLiana
2
+ class OidcConfigurationRetriever
3
+ def self.retrieve()
4
+ response = ForestLiana::ForestApiRequester.get('/oidc/.well-known/openid-configuration')
5
+ if response.is_a?(Net::HTTPOK)
6
+ return JSON.parse(response.body)
7
+ else
8
+ raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:OIDC_CONFIGURATION_RETRIEVAL_FAILED]
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,67 @@
1
+ require 'json'
2
+ require 'json/jwt'
3
+
4
+ module ForestLiana
5
+ class OidcDynamicClientRegistrator
6
+ def self.is_standard_body_error(response)
7
+ result = false
8
+ begin
9
+ jsonbody
10
+
11
+ if (!response['body'].is_a?(Object) || response['body'].is_a?(StringIO))
12
+ jsonbody = JSON.parse(response['body'])
13
+ else
14
+ jsonbody = response['body']
15
+ end
16
+
17
+ result = jsonbody['error'].is_a?(String) && jsonbody['error'].length > 0
18
+
19
+ if (result)
20
+ response['body'] = jsonbody
21
+ end
22
+ rescue
23
+ {}
24
+ end
25
+
26
+ return result
27
+ end
28
+
29
+ def self.process_response(response, expected = {})
30
+ statusCode = expected[:statusCode] || 200
31
+ body = expected[:body] || true
32
+
33
+ if (response.code.to_i != statusCode.to_i)
34
+ if (is_standard_body_error(response))
35
+ raise response['body']
36
+ end
37
+
38
+ raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:REGISTRATION_FAILED] + response.body
39
+ end
40
+
41
+ if (body && !response.body)
42
+ raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:REGISTRATION_FAILED] + response.body
43
+ end
44
+
45
+ return response.body
46
+ end
47
+
48
+ def self.authorization_header_value(token, tokenType = 'Bearer')
49
+ return "#{tokenType} #{token}"
50
+ end
51
+
52
+ def self.register(metadata)
53
+ initial_access_token = ForestLiana.env_secret
54
+
55
+ response = ForestLiana::ForestApiRequester.post(
56
+ metadata[:registration_endpoint],
57
+ body: metadata,
58
+ headers: initial_access_token ? {
59
+ Authorization: authorization_header_value(initial_access_token),
60
+ } : {},
61
+ )
62
+
63
+ responseBody = process_response(response, { :statusCode => 201, :bearer => true })
64
+ return JSON.parse(responseBody)
65
+ end
66
+ end
67
+ end
@@ -44,7 +44,7 @@ module ForestLiana
44
44
  @allowed = @smart_action_permissions['allowed']
45
45
  @users = @smart_action_permissions['users']
46
46
 
47
- return @allowed && (@users.nil?|| @users.include?(@user_id.to_i));
47
+ return @allowed && (@users.nil?|| @users.include?(@user_id.to_i))
48
48
  end
49
49
 
50
50
  def collection_list_allowed?(scope_permissions)
@@ -2,11 +2,11 @@ module ForestLiana
2
2
  class QueryStatGetter
3
3
  attr_accessor :record
4
4
 
5
- CHART_TYPE_VALUE = 'Value';
6
- CHART_TYPE_PIE = 'Pie';
7
- CHART_TYPE_LINE = 'Line';
8
- CHART_TYPE_LEADERBOARD = 'Leaderboard';
9
- CHART_TYPE_OBJECTIVE = 'Objective';
5
+ CHART_TYPE_VALUE = 'Value'
6
+ CHART_TYPE_PIE = 'Pie'
7
+ CHART_TYPE_LINE = 'Line'
8
+ CHART_TYPE_LEADERBOARD = 'Leaderboard'
9
+ CHART_TYPE_OBJECTIVE = 'Objective'
10
10
 
11
11
  def initialize(params)
12
12
  @params = params
@@ -0,0 +1,27 @@
1
+ EXPIRATION_IN_SECONDS = 14.days
2
+
3
+ module ForestLiana
4
+ class Token
5
+ REGEX_COOKIE_SESSION_TOKEN = /forest_session_token=([^;]*)/;
6
+
7
+ def self.expiration_in_days
8
+ Time.current + EXPIRATION_IN_SECONDS
9
+ end
10
+
11
+ def self.expiration_in_seconds
12
+ return Time.now.to_i + EXPIRATION_IN_SECONDS
13
+ end
14
+
15
+ def self.create_token(user, rendering_id)
16
+ return JWT.encode({
17
+ id: user['id'],
18
+ email: user['email'],
19
+ first_name: user['first_name'],
20
+ last_name: user['last_name'],
21
+ team: user['teams'][0],
22
+ rendering_id: rendering_id,
23
+ exp: expiration_in_seconds()
24
+ }, ForestLiana.auth_secret, 'HS256')
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ module ForestLiana
2
+ MESSAGES = {
3
+ CONFIGURATION: {
4
+ AUTH_SECRET_MISSING: "Your Forest authSecret seems to be missing. Can you check that you properly set a Forest authSecret in the Forest initializer?",
5
+ },
6
+ SERVER_TRANSACTION: {
7
+ SECRET_AND_RENDERINGID_INCONSISTENT: "Cannot retrieve the project you're trying to unlock. The envSecret and renderingId seems to be missing or inconsistent.",
8
+ SERVER_DOWN: "Cannot retrieve the data from the Forest server. Forest API seems to be down right now.",
9
+ SECRET_NOT_FOUND: "Cannot retrieve the data from the Forest server. Can you check that you properly copied the Forest envSecret in the Liana initializer?",
10
+ UNEXPECTED: "Cannot retrieve the data from the Forest server. An error occured in Forest API.",
11
+ INVALID_STATE_MISSING: "Invalid response from the authentication server: the state parameter is missing",
12
+ INVALID_STATE_FORMAT: "Invalid response from the authentication server: the state parameter is not at the right format",
13
+ INVALID_STATE_RENDERING_ID: "Invalid response from the authentication server: the state does not contain a renderingId",
14
+ MISSING_RENDERING_ID: "Authentication request must contain a renderingId",
15
+ INVALID_RENDERING_ID: "The parameter renderingId is not valid",
16
+ REGISTRATION_FAILED: "The registration to the authentication API failed, response: ",
17
+ OIDC_CONFIGURATION_RETRIEVAL_FAILED: "Failed to retrieve the provider's configuration.",
18
+ }
19
+ }
20
+ end
@@ -4,6 +4,11 @@ ForestLiana::Engine.routes.draw do
4
4
  # Onboarding
5
5
  get '/' => 'apimaps#index'
6
6
 
7
+ # Authentication
8
+ post 'authentication' => 'authentication#start_authentication'
9
+ get 'authentication/callback' => 'authentication#authentication_callback'
10
+ post 'authentication/logout' => 'authentication#logout'
11
+
7
12
  # Session
8
13
  post 'sessions' => 'sessions#create_with_password'
9
14
  post 'sessions-google' => 'sessions#create_with_google'
@@ -16,6 +16,7 @@ module ForestLiana
16
16
 
17
17
  mattr_accessor :env_secret
18
18
  mattr_accessor :auth_secret
19
+ mattr_accessor :application_url
19
20
  mattr_accessor :integrations
20
21
  mattr_accessor :apimap
21
22
  mattr_accessor :allowed_users
@@ -621,7 +621,7 @@ module ForestLiana
621
621
  end
622
622
 
623
623
  def forest_url
624
- ENV['FOREST_URL'] || 'https://api.forestadmin.com';
624
+ ENV['FOREST_URL'] || 'https://api.forestadmin.com'
625
625
  end
626
626
 
627
627
  def database_type
@@ -148,7 +148,7 @@ module ForestLiana::Collection
148
148
  # TODO: Remove once lianas prior to 2.0.0 are not supported anymore.
149
149
  model = ForestLiana.models.find { |collection| collection.try(:table_name) == collection_name.to_s }
150
150
  if model
151
- collection_name_new = ForestLiana.name_for(model);
151
+ collection_name_new = ForestLiana.name_for(model)
152
152
  FOREST_LOGGER.warn "DEPRECATION WARNING: Collection names are now based on the models " \
153
153
  "names. Please rename the collection '#{collection_name.to_s}' of your Forest " \
154
154
  "customisation in '#{collection_name_new}'."
@@ -158,7 +158,7 @@ module ForestLiana::Collection
158
158
  # TODO: Remove once lianas prior to 2.0.0 are not supported anymore.
159
159
  model = ForestLiana.names_old_overriden.invert[collection_name.to_s]
160
160
  if model
161
- collection_name_new = ForestLiana.name_for(model);
161
+ collection_name_new = ForestLiana.name_for(model)
162
162
  FOREST_LOGGER.warn "DEPRECATION WARNING: Collection names are now based on the models " \
163
163
  "names. Please rename the collection '#{collection_name.to_s}' of your Forest " \
164
164
  "customisation in '#{collection_name_new}'."
@@ -16,8 +16,17 @@ module ForestLiana
16
16
  begin
17
17
  rack_cors_class = Rack::Cors
18
18
  rack_cors_class = 'Rack::Cors' if Rails::VERSION::MAJOR < 5
19
+ null_regex = Regexp.new(/\Anull\z/)
19
20
 
20
21
  config.middleware.insert_before 0, rack_cors_class do
22
+ allow do
23
+ hostnames = [null_regex, 'localhost:4200', /\A.*\.forestadmin\.com\z/]
24
+ hostnames += ENV['CORS_ORIGINS'].split(',') if ENV['CORS_ORIGINS']
25
+
26
+ origins hostnames
27
+ resource ForestLiana::AuthenticationController::PUBLIC_ROUTES[1], headers: :any, methods: :any, credentials: true, max_age: 86400 # NOTICE: 1 day
28
+ end
29
+
21
30
  allow do
22
31
  hostnames = ['localhost:4200', /\A.*\.forestadmin\.com\z/]
23
32
  hostnames += ENV['CORS_ORIGINS'].split(',') if ENV['CORS_ORIGINS']
@@ -20,7 +20,7 @@ module ForestLiana
20
20
  result << ", "
21
21
  end
22
22
 
23
- result << pretty_print(item, is_primary_value ? "#{indentation} " : indentation);
23
+ result << pretty_print(item, is_primary_value ? "#{indentation} " : indentation)
24
24
  end
25
25
 
26
26
  result << "\n#{indentation}" if is_primary_value && !is_small
@@ -1,3 +1,3 @@
1
1
  module ForestLiana
2
- VERSION = "6.0.0-beta.1"
2
+ VERSION = "6.0.0-beta.2"
3
3
  end
@@ -1,2 +1,3 @@
1
1
  ForestLiana.env_secret = 'env_secret_test'
2
2
  ForestLiana.auth_secret = 'auth_secret_test'
3
+ ForestLiana.application_url = 'http://localhost:3000'
@@ -0,0 +1,107 @@
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
+ headers = {
29
+ 'Accept' => 'application/json',
30
+ 'Content-Type' => 'application/json',
31
+ }
32
+
33
+ describe "POST /authentication" do
34
+ before() do
35
+ post ForestLiana::Engine.routes.url_helpers.authentication_path, { :renderingId => 42 }, :headers => headers
36
+ end
37
+
38
+ it "should respond with a 302 code" do
39
+ expect(response).to have_http_status(302)
40
+ end
41
+
42
+ it "should return a valid authentication url" do
43
+ 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')
44
+ end
45
+ end
46
+
47
+ describe "GET /authentication/callback" do
48
+ before() do
49
+ response = '{"data":{"id":666,"attributes":{"first_name":"Alice","last_name":"Doe","email":"alice@forestadmin.com","teams":[1,2,3]}}}'
50
+ allow(ForestLiana::ForestApiRequester).to receive(:get).with(
51
+ "/liana/v2/renderings/42/authorization", { :headers => { "forest-token" => "THE-ACCESS-TOKEN" }, :query=> {} }
52
+ ).and_return(
53
+ instance_double(HTTParty::Response, :body => response, :code => 200)
54
+ )
55
+
56
+ get ForestLiana::Engine.routes.url_helpers.authentication_callback_path + "?code=THE-CODE&state=#{URI.escape('{"renderingId":42}', Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))}"
57
+ end
58
+
59
+ it "should respond with a 200 code" do
60
+ expect(response).to have_http_status(200)
61
+ end
62
+
63
+ it "should return a valid authentication token" do
64
+ session_cookie = response.headers['set-cookie']
65
+ expect(session_cookie).to match(/^forest_session_token=[^;]+; path=\/; expires=[^;]+; secure; HttpOnly$/)
66
+
67
+ token = session_cookie.match(/^forest_session_token=([^;]+);/)[1]
68
+ decoded = JWT.decode(token, ForestLiana.auth_secret, true, { algorithm: 'HS256' })[0]
69
+
70
+ expected_token_data = {
71
+ "id" => 666,
72
+ "email" => 'alice@forestadmin.com',
73
+ "rendering_id" => "42",
74
+ "first_name" => 'Alice',
75
+ "last_name" => 'Doe',
76
+ "team" => 1,
77
+ }
78
+
79
+ expect(decoded).to include(expected_token_data)
80
+ expect(JSON.parse(response.body, :symbolize_names => true)).to eq({ token: token, tokenData: decoded.deep_symbolize_keys! })
81
+ expect(response).to have_http_status(200)
82
+ end
83
+ end
84
+
85
+ describe "POST /authentication/logout" do
86
+ before() do
87
+ cookies['forest_session_token'] = {
88
+ value: 'eyJhbGciOiJIUzI1NiJ9.eyJpZCI6NjY2LCJlbWFpbCI6ImFsaWNlQGZvcmVzdGFkbWluLmNvbSIsImZpcnN0X25hbWUiOiJBbGljZSIsImxhc3RfbmFtZSI6IkRvZSIsInRlYW0iOjEsInJlbmRlcmluZ19pZCI6IjQyIiwiZXhwIjoxNjA4MDQ5MTI2fQ.5xaMxjUjE3wKldBsj3wW0BP9GHnnMqQi2Kpde8cIHEw',
89
+ path: '/',
90
+ expires: Time.now.to_i + 14.days,
91
+ secure: true,
92
+ httponly: true
93
+ }
94
+ post ForestLiana::Engine.routes.url_helpers.authentication_logout_path, { :renderingId => 42 }, :headers => headers
95
+ cookies.delete('forest_session_token')
96
+ end
97
+
98
+ it "should respond with a 204 code" do
99
+ expect(response).to have_http_status(204)
100
+ end
101
+
102
+ it "should invalidate token from browser" do
103
+ invalidated_session_cookie = response.headers['set-cookie']
104
+ expect(invalidated_session_cookie).to match(/^forest_session_token=[^;]+; path=\/; expires=Thu, 01 Jan 1970 00:00:00 -0000; secure; HttpOnly$/)
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,55 @@
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
+ headers = {
24
+ 'Accept' => 'application/json',
25
+ 'Content-Type' => 'application/json',
26
+ }
27
+
28
+ describe "POST /forest/sessions-google" do
29
+ before() do
30
+ post ForestLiana::Engine.routes.url_helpers.sessions_google_path, { :renderingId => 42, :forestToken => "google-access-token" }, :headers => headers
31
+ end
32
+
33
+ it "should respond with a 200 code" do
34
+ expect(response).to have_http_status(200)
35
+ end
36
+
37
+ it "should return a valid authentication token" do
38
+ response_body = JSON.parse(response.body, :symbolize_names => true)
39
+ expect(response_body).to have_key(:token)
40
+
41
+ token = response_body[:token]
42
+ decoded = JWT.decode(token, ForestLiana.auth_secret, true, { algorithm: 'HS256' })[0]
43
+
44
+ expected_token_data = {
45
+ "id" => '654',
46
+ "email" => 'user@email.com',
47
+ "first_name" => 'FirstName',
48
+ "last_name" => 'LastName',
49
+ "rendering_id" => "42",
50
+ "team" => 'Operations'
51
+ }
52
+ expect(decoded).to include(expected_token_data);
53
+ end
54
+ end
55
+ end
@@ -9,7 +9,7 @@ module ForestLiana
9
9
 
10
10
  expect(collection.fields.map { |field| field[:field] }).to eq(
11
11
  ["id", "name", "created_at", "updated_at", "trees"]
12
- );
12
+ )
13
13
  end
14
14
  end
15
15
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forest_liana
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.0.pre.beta.1
4
+ version: 6.0.0.pre.beta.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sandro Munda
@@ -178,6 +178,48 @@ dependencies:
178
178
  - - ">="
179
179
  - !ruby/object:Gem::Version
180
180
  version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: json
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :runtime
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: json-jwt
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :runtime
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: openid_connect
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :runtime
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
181
223
  description: Forest is a modern admin interface that works on all major web frameworks.
182
224
  forest_liana is the gem that makes Forest admin work on any Rails application (Rails
183
225
  >= 4.0).
@@ -197,6 +239,7 @@ files:
197
239
  - app/controllers/forest_liana/apimaps_controller.rb
198
240
  - app/controllers/forest_liana/application_controller.rb
199
241
  - app/controllers/forest_liana/associations_controller.rb
242
+ - app/controllers/forest_liana/authentication_controller.rb
200
243
  - app/controllers/forest_liana/base_controller.rb
201
244
  - app/controllers/forest_liana/devise_controller.rb
202
245
  - app/controllers/forest_liana/intercom_controller.rb
@@ -230,6 +273,7 @@ files:
230
273
  - app/serializers/forest_liana/stripe_payment_serializer.rb
231
274
  - app/serializers/forest_liana/stripe_subscription_serializer.rb
232
275
  - app/services/forest_liana/apimap_sorter.rb
276
+ - app/services/forest_liana/authentication.rb
233
277
  - app/services/forest_liana/authorization_getter.rb
234
278
  - app/services/forest_liana/base_getter.rb
235
279
  - app/services/forest_liana/belongs_to_updater.rb
@@ -251,6 +295,9 @@ files:
251
295
  - app/services/forest_liana/login_handler.rb
252
296
  - app/services/forest_liana/mixpanel_last_events_getter.rb
253
297
  - app/services/forest_liana/objective_stat_getter.rb
298
+ - app/services/forest_liana/oidc_client_manager.rb
299
+ - app/services/forest_liana/oidc_configuration_retriever.rb
300
+ - app/services/forest_liana/oidc_dynamic_client_registrator.rb
254
301
  - app/services/forest_liana/operator_date_interval_parser.rb
255
302
  - app/services/forest_liana/permissions_checker.rb
256
303
  - app/services/forest_liana/permissions_getter.rb
@@ -275,11 +322,13 @@ files:
275
322
  - app/services/forest_liana/stripe_sources_getter.rb
276
323
  - app/services/forest_liana/stripe_subscription_getter.rb
277
324
  - app/services/forest_liana/stripe_subscriptions_getter.rb
325
+ - app/services/forest_liana/token.rb
278
326
  - app/services/forest_liana/two_factor_registration_confirmer.rb
279
327
  - app/services/forest_liana/user_secret_creator.rb
280
328
  - app/services/forest_liana/value_stat_getter.rb
281
329
  - app/views/layouts/forest_liana/application.html.erb
282
330
  - config/initializers/arel-helpers.rb
331
+ - config/initializers/error-messages.rb
283
332
  - config/initializers/errors.rb
284
333
  - config/initializers/logger.rb
285
334
  - config/initializers/time_formats.rb
@@ -339,7 +388,9 @@ files:
339
388
  - spec/helpers/forest_liana/query_helper_spec.rb
340
389
  - spec/helpers/forest_liana/schema_helper_spec.rb
341
390
  - spec/rails_helper.rb
391
+ - spec/requests/authentications_spec.rb
342
392
  - spec/requests/resources_spec.rb
393
+ - spec/requests/sessions_spec.rb
343
394
  - spec/services/forest_liana/apimap_sorter_spec.rb
344
395
  - spec/services/forest_liana/filters_parser_spec.rb
345
396
  - spec/services/forest_liana/ip_whitelist_checker_spec.rb
@@ -556,6 +607,8 @@ test_files:
556
607
  - spec/services/forest_liana/apimap_sorter_spec.rb
557
608
  - spec/services/forest_liana/filters_parser_spec.rb
558
609
  - spec/spec_helper.rb
610
+ - spec/requests/sessions_spec.rb
611
+ - spec/requests/authentications_spec.rb
559
612
  - spec/requests/resources_spec.rb
560
613
  - spec/dummy/README.rdoc
561
614
  - spec/dummy/app/views/layouts/application.html.erb