forest_liana 6.0.0.pre.beta.1 → 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 (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