forest_admin_agent 1.0.0.pre.beta.21

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 (62) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +5 -0
  4. data/README.md +35 -0
  5. data/Rakefile +10 -0
  6. data/forest_admin +1 -0
  7. data/forest_admin_agent.gemspec +48 -0
  8. data/lib/forest_admin_agent/auth/auth_manager.rb +50 -0
  9. data/lib/forest_admin_agent/auth/oauth2/forest_provider.rb +62 -0
  10. data/lib/forest_admin_agent/auth/oauth2/forest_resource_owner.rb +42 -0
  11. data/lib/forest_admin_agent/auth/oauth2/oidc_config.rb +29 -0
  12. data/lib/forest_admin_agent/auth/oidc_client_manager.rb +71 -0
  13. data/lib/forest_admin_agent/builder/agent_factory.rb +77 -0
  14. data/lib/forest_admin_agent/facades/container.rb +23 -0
  15. data/lib/forest_admin_agent/facades/whitelist.rb +13 -0
  16. data/lib/forest_admin_agent/http/Exceptions/authentication_open_id_client.rb +16 -0
  17. data/lib/forest_admin_agent/http/Exceptions/http_exception.rb +17 -0
  18. data/lib/forest_admin_agent/http/Exceptions/not_found_error.rb +15 -0
  19. data/lib/forest_admin_agent/http/forest_admin_api_requester.rb +28 -0
  20. data/lib/forest_admin_agent/http/router.rb +52 -0
  21. data/lib/forest_admin_agent/routes/abstract_authenticated_route.rb +25 -0
  22. data/lib/forest_admin_agent/routes/abstract_related_route.rb +12 -0
  23. data/lib/forest_admin_agent/routes/abstract_route.rb +27 -0
  24. data/lib/forest_admin_agent/routes/resources/count.rb +41 -0
  25. data/lib/forest_admin_agent/routes/resources/delete.rb +51 -0
  26. data/lib/forest_admin_agent/routes/resources/list.rb +38 -0
  27. data/lib/forest_admin_agent/routes/resources/related/associate_related.rb +63 -0
  28. data/lib/forest_admin_agent/routes/resources/related/count_related.rb +56 -0
  29. data/lib/forest_admin_agent/routes/resources/related/dissociate_related.rb +97 -0
  30. data/lib/forest_admin_agent/routes/resources/related/list_related.rb +54 -0
  31. data/lib/forest_admin_agent/routes/resources/related/update_related.rb +102 -0
  32. data/lib/forest_admin_agent/routes/resources/show.rb +44 -0
  33. data/lib/forest_admin_agent/routes/resources/store.rb +51 -0
  34. data/lib/forest_admin_agent/routes/resources/update.rb +42 -0
  35. data/lib/forest_admin_agent/routes/security/authentication.rb +95 -0
  36. data/lib/forest_admin_agent/routes/system/health_check.rb +22 -0
  37. data/lib/forest_admin_agent/serializer/forest_serializer.rb +176 -0
  38. data/lib/forest_admin_agent/serializer/forest_serializer_override.rb +103 -0
  39. data/lib/forest_admin_agent/services/ip_whitelist.rb +100 -0
  40. data/lib/forest_admin_agent/services/logger_service.rb +20 -0
  41. data/lib/forest_admin_agent/utils/condition_tree_parser.rb +57 -0
  42. data/lib/forest_admin_agent/utils/error_messages.rb +38 -0
  43. data/lib/forest_admin_agent/utils/id.rb +48 -0
  44. data/lib/forest_admin_agent/utils/query_string_parser.rb +89 -0
  45. data/lib/forest_admin_agent/utils/schema/frontend_filterable.rb +73 -0
  46. data/lib/forest_admin_agent/utils/schema/generator_collection.rb +35 -0
  47. data/lib/forest_admin_agent/utils/schema/generator_field.rb +183 -0
  48. data/lib/forest_admin_agent/utils/schema/schema_emitter.rb +103 -0
  49. data/lib/forest_admin_agent/version.rb +3 -0
  50. data/lib/forest_admin_agent.rb +11 -0
  51. data/sig/forest_admin_agent/auth/auth_manager.rbs +14 -0
  52. data/sig/forest_admin_agent/auth/oauth2/forest_provider.rbs +16 -0
  53. data/sig/forest_admin_agent/auth/oauth2/forest_resource_owner.rbs +15 -0
  54. data/sig/forest_admin_agent/auth/oidc_client_manager.rbs +15 -0
  55. data/sig/forest_admin_agent/builder/agent_factory.rbs +21 -0
  56. data/sig/forest_admin_agent/facades/container.rbs +9 -0
  57. data/sig/forest_admin_agent/http/router.rbs +9 -0
  58. data/sig/forest_admin_agent/routes/abstract_route.rbs +12 -0
  59. data/sig/forest_admin_agent/routes/security/authentication.rbs +14 -0
  60. data/sig/forest_admin_agent/routes/system/health_check.rbs +10 -0
  61. data/sig/forest_admin_agent.rbs +4 -0
  62. metadata +279 -0
@@ -0,0 +1,95 @@
1
+ require 'jwt'
2
+
3
+ module ForestAdminAgent
4
+ module Routes
5
+ module Security
6
+ class Authentication < AbstractRoute
7
+ include ForestAdminAgent::Builder
8
+ include ForestAdminAgent::Http::Exceptions
9
+
10
+ def setup_routes
11
+ add_route(
12
+ 'forest_authentication',
13
+ 'POST',
14
+ '/authentication', ->(args) { handle_authentication(args) }
15
+ )
16
+ add_route(
17
+ 'forest_authentication-callback',
18
+ 'GET',
19
+ '/authentication/callback', ->(args) { handle_authentication_callback(args) }
20
+ )
21
+ add_route(
22
+ 'forest_logout',
23
+ 'POST',
24
+ '/authentication/logout', ->(args) { handle_authentication_logout(args) }
25
+ )
26
+
27
+ self
28
+ end
29
+
30
+ def handle_authentication(args = {})
31
+ if args.dig(:headers, 'action_dispatch.remote_ip')
32
+ Facades::Whitelist.check_ip(args[:headers]['action_dispatch.remote_ip'].to_s)
33
+ end
34
+ rendering_id = get_and_check_rendering_id args[:params]
35
+
36
+ {
37
+ content: {
38
+ authorizationUrl: auth.start(rendering_id)
39
+ }
40
+ }
41
+ end
42
+
43
+ def handle_authentication_callback(args = {})
44
+ if args[:params].key?(:error)
45
+ raise AuthenticationOpenIdClient.new(args[:params][:error], args[:params][:error_description],
46
+ args[:params][:state])
47
+ end
48
+
49
+ if args.dig(:headers, 'action_dispatch.remote_ip')
50
+ Facades::Whitelist.check_ip(args[:headers]['action_dispatch.remote_ip'].to_s)
51
+ end
52
+ token = auth.verify_code_and_generate_token(args[:params])
53
+ token_data = JWT.decode(
54
+ token,
55
+ Facades::Container.cache(:auth_secret),
56
+ true,
57
+ { algorithm: 'HS256' }
58
+ )[0]
59
+
60
+ {
61
+ content: {
62
+ token: token,
63
+ tokenData: token_data
64
+ }
65
+ }
66
+ end
67
+
68
+ def handle_authentication_logout(_args = {})
69
+ {
70
+ content: nil,
71
+ status: 204
72
+ }
73
+ end
74
+
75
+ def auth
76
+ ForestAdminAgent::Auth::AuthManager.new
77
+ end
78
+
79
+ protected
80
+
81
+ def get_and_check_rendering_id(params)
82
+ raise Error, ForestAdminAgent::Utils::ErrorMessages::MISSING_RENDERING_ID unless params['renderingId']
83
+
84
+ begin
85
+ Integer(params['renderingId'])
86
+ rescue ArgumentError
87
+ raise Error, ForestAdminAgent::Utils::ErrorMessages::INVALID_RENDERING_ID
88
+ end
89
+
90
+ params['renderingId'].to_i
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,22 @@
1
+ module ForestAdminAgent
2
+ module Routes
3
+ module System
4
+ class HealthCheck < AbstractRoute
5
+ include ForestAdminAgent::Builder
6
+ def setup_routes
7
+ add_route('forest', 'GET', '/', ->(args) { handle_request(args) })
8
+
9
+ self
10
+ end
11
+
12
+ def handle_request(_args = {})
13
+ if AgentFactory.instance.container.resolve(:cache).get('config')[:is_production]
14
+ AgentFactory.instance.send_schema(force: true)
15
+ end
16
+
17
+ { content: nil, status: 204 }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,176 @@
1
+ require 'jsonapi-serializers'
2
+
3
+ module ForestAdminAgent
4
+ module Serializer
5
+ class ForestSerializer
6
+ include JSONAPI::Serializer
7
+
8
+ attr_accessor :attributes_map
9
+ attr_accessor :to_one_associations
10
+ attr_accessor :to_many_associations
11
+
12
+ JSONAPI::Serializer.send(:include, ForestSerializerOverride)
13
+
14
+ def initialize(object, options = nil)
15
+ super
16
+ end
17
+
18
+ def base_url
19
+ Facades::Container.cache(:prefix)
20
+ end
21
+
22
+ def type
23
+ class_name = object.class.name
24
+ @@class_names[class_name] ||= class_name.demodulize.underscore.freeze
25
+ end
26
+
27
+ def format_name(attribute_name)
28
+ attribute_name.to_s
29
+ end
30
+
31
+ def add_attribute(name, options = {}, &block)
32
+ @attributes_map ||= {}
33
+ @attributes_map[name] = format_field(name, options)
34
+ end
35
+
36
+ def format_field(name, options)
37
+ {
38
+ attr_or_block: block_given? ? block : name,
39
+ options: options,
40
+ }
41
+ end
42
+
43
+ def attributes
44
+ forest_collection = ForestAdminAgent::Facades::Container.datasource.collection(object.class.name.demodulize.underscore)
45
+ fields = forest_collection.fields.select { |_field_name, field| field.type == 'Column' }
46
+ fields.each { |field_name, _field| add_attribute(field_name) }
47
+ return {} if attributes_map.nil?
48
+ attributes = {}
49
+
50
+ attributes_map.each do |attribute_name, attr_data|
51
+ next if !should_include_attr?(attribute_name, attr_data)
52
+ value = evaluate_attr_or_block(attribute_name, attr_data[:attr_or_block])
53
+ attributes[format_name(attribute_name)] = value
54
+ end
55
+ attributes
56
+ end
57
+
58
+ def evaluate_attr_or_block(attribute_name, attr_or_block)
59
+ if attr_or_block.is_a?(Proc)
60
+ # A custom block was given, call it to get the value.
61
+ instance_eval(&attr_or_block)
62
+ else
63
+ # Default behavior, call a method by the name of the attribute.
64
+ begin
65
+ object.try(attr_or_block)
66
+ rescue
67
+ nil
68
+ end
69
+ end
70
+ end
71
+
72
+ def add_to_one_association(name, options = {}, &block)
73
+ options[:include_links] = options.fetch(:include_links, true)
74
+ options[:include_data] = options.fetch(:include_data, false)
75
+ @to_one_associations ||= {}
76
+ @to_one_associations[name] = format_field(name, options)
77
+ end
78
+
79
+ def has_one_relationships
80
+ return {} if @to_one_associations.nil?
81
+ data = {}
82
+ @to_one_associations.each do |attribute_name, attr_data|
83
+ next if !should_include_attr?(attribute_name, attr_data)
84
+ data[attribute_name.to_sym] = attr_data
85
+ end
86
+ data
87
+ end
88
+
89
+ def has_many_relationships
90
+ return {} if @to_many_associations.nil?
91
+ data = {}
92
+ @to_many_associations.each do |attribute_name, attr_data|
93
+ next if !should_include_attr?(attribute_name, attr_data)
94
+ data[attribute_name.to_sym] = attr_data
95
+ end
96
+ data
97
+ end
98
+
99
+ def add_to_many_association(name, options = {}, &block)
100
+ options[:include_links] = options.fetch(:include_links, true)
101
+ options[:include_data] = options.fetch(:include_data, false)
102
+ @to_many_associations ||= {}
103
+ @to_many_associations[name] = format_field(name, options)
104
+ end
105
+
106
+ def relationships
107
+ forest_collection = ForestAdminAgent::Facades::Container.datasource.collection(object.class.name.demodulize.underscore)
108
+ relations_to_many = forest_collection.fields.select { |_field_name, field| field.type == 'OneToMany' || field.type == 'ManyToMany' }
109
+ relations_to_one = forest_collection.fields.select { |_field_name, field| field.type == 'OneToOne' || field.type == 'ManyToOne' }
110
+
111
+ relations_to_one.each { |field_name, _field| add_to_one_association(field_name) }
112
+
113
+ data = {}
114
+ has_one_relationships.each do |attribute_name, attr_data|
115
+ formatted_attribute_name = format_name(attribute_name)
116
+ data[formatted_attribute_name] = {}
117
+
118
+ if attr_data[:options][:include_links]
119
+ links_self = relationship_self_link(attribute_name)
120
+ links_related = relationship_related_link(attribute_name)
121
+ data[formatted_attribute_name]['links'] = {} if links_self || links_related
122
+ data[formatted_attribute_name]['links']['related'] = {} if links_self
123
+ data[formatted_attribute_name]['links']['related']['href'] = links_self if links_self
124
+ end
125
+
126
+ object = has_one_relationship(attribute_name, attr_data)
127
+ if object.nil?
128
+ data[formatted_attribute_name]['data'] = nil
129
+ else
130
+ related_object_serializer = ForestSerializer.new(object, @options)
131
+ data[formatted_attribute_name]['data'] = {
132
+ 'type' => related_object_serializer.type.to_s,
133
+ 'id' => related_object_serializer.id.to_s,
134
+ }
135
+ end
136
+ end
137
+
138
+ relations_to_many.each { |field_name, _field| add_to_many_association(field_name) }
139
+
140
+ has_many_relationships.each do |attribute_name, attr_data|
141
+ formatted_attribute_name = format_name(attribute_name)
142
+ data[formatted_attribute_name] = {}
143
+
144
+ if attr_data[:options][:include_links]
145
+ links_self = relationship_self_link(attribute_name)
146
+ links_related = relationship_related_link(attribute_name)
147
+ data[formatted_attribute_name]['links'] = {} if links_self || links_related
148
+ data[formatted_attribute_name]['links']['related'] = {} if links_self
149
+ data[formatted_attribute_name]['links']['related']['href'] = links_self if links_self
150
+ end
151
+
152
+ if @_include_linkages.include?(formatted_attribute_name) || attr_data[:options][:include_data]
153
+ data[formatted_attribute_name]['data'] = []
154
+ objects = has_many_relationship(attribute_name, attr_data) || []
155
+ objects.each do |obj|
156
+ related_object_serializer = JSONAPI::Serializer.find_serializer(obj, @options)
157
+ data[formatted_attribute_name]['data'] << {
158
+ 'type' => related_object_serializer.type.to_s,
159
+ 'id' => related_object_serializer.id.to_s,
160
+ }
161
+ end
162
+ end
163
+ end
164
+ data
165
+ end
166
+
167
+ def relationship_self_link(attribute_name)
168
+ "/#{self_link}/relationships/#{format_name(attribute_name)}"
169
+ end
170
+
171
+ def relationship_related_link(attribute_name)
172
+ "/#{self_link}/#{format_name(attribute_name)}"
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,103 @@
1
+ module ForestAdminAgent
2
+ module Serializer
3
+ module ForestSerializerOverride
4
+ def self.included(base)
5
+ base.instance_eval do
6
+ def self.find_serializer_class_name(object, options)
7
+ return options[:serializer].to_s if options[:serializer]
8
+ return "#{options[:namespace]}::#{object.class.name}Serializer" if options[:namespace]
9
+ return object.jsonapi_serializer_class_name.to_s if object.respond_to?(:jsonapi_serializer_class_name)
10
+
11
+ "#{object.class.name}Serializer"
12
+ end
13
+
14
+ def self.find_recursive_relationships(root_object, root_inclusion_tree, results, options)
15
+ root_inclusion_tree.each do |attribute_name, child_inclusion_tree|
16
+ # Skip the sentinal value, but we need to preserve it for siblings.
17
+ next if attribute_name == :_include
18
+
19
+ serializer = JSONAPI::Serializer.find_serializer(root_object, options)
20
+ serializer.relationships
21
+ unformatted_attr_name = serializer.unformat_name(attribute_name).to_sym
22
+
23
+ # We know the name of this relationship, but we don't know where it is stored internally.
24
+ # Check if it is a has_one or has_many relationship.
25
+ object = nil
26
+ is_collection = false
27
+ is_valid_attr = false
28
+ if serializer.has_one_relationships.key?(unformatted_attr_name)
29
+ is_valid_attr = true
30
+ attr_data = serializer.has_one_relationships[unformatted_attr_name]
31
+ object = serializer.has_one_relationship(unformatted_attr_name, attr_data)
32
+ elsif serializer.has_many_relationships.key?(unformatted_attr_name)
33
+ is_valid_attr = true
34
+ is_collection = true
35
+ attr_data = serializer.has_many_relationships[unformatted_attr_name]
36
+ object = serializer.has_many_relationship(unformatted_attr_name, attr_data)
37
+ end
38
+
39
+ unless is_valid_attr
40
+ raise JSONAPI::Serializer::InvalidIncludeError, "'#{attribute_name}' is not a valid include."
41
+ end
42
+
43
+ if attribute_name != serializer.format_name(attribute_name)
44
+ expected_name = serializer.format_name(attribute_name)
45
+
46
+ raise JSONAPI::Serializer::InvalidIncludeError,
47
+ "'#{attribute_name}' is not a valid include. Did you mean '#{expected_name}' ?"
48
+ end
49
+
50
+ # We're finding relationships for compound documents, so skip anything that doesn't exist.
51
+ next if object.nil?
52
+
53
+ # Full linkage: a request for comments.author MUST automatically include comments
54
+ # in the response.
55
+ objects = is_collection ? object : [object]
56
+ if child_inclusion_tree[:_include] == true
57
+ # Include the current level objects if the _include attribute exists.
58
+ # If it is not set, that indicates that this is an inner path and not a leaf and will
59
+ # be followed by the recursion below.
60
+ objects.each do |obj|
61
+ obj_serializer = JSONAPI::Serializer.find_serializer(obj, options)
62
+ # Use keys of ['posts', '1'] for the results to enforce uniqueness.
63
+ # Spec: A compound document MUST NOT include more than one resource object for each
64
+ # type and id pair.
65
+ # http://jsonapi.org/format/#document-structure-compound-documents
66
+ key = [obj_serializer.type, obj_serializer.id]
67
+
68
+ # This is special: we know at this level if a child of this parent will also been
69
+ # included in the compound document, so we can compute exactly what linkages should
70
+ # be included by the object at this level. This satisfies this part of the spec:
71
+ #
72
+ # Spec: Resource linkage in a compound document allows a client to link together
73
+ # all of the included resource objects without having to GET any relationship URLs.
74
+ # http://jsonapi.org/format/#document-structure-resource-relationships
75
+ current_child_includes = []
76
+ inclusion_names = child_inclusion_tree.keys.reject { |k| k == :_include }
77
+ inclusion_names.each do |inclusion_name|
78
+ current_child_includes << inclusion_name if child_inclusion_tree[inclusion_name][:_include]
79
+ end
80
+
81
+ # Special merge: we might see this object multiple times in the course of recursion,
82
+ # so merge the include_linkages each time we see it to load all the relevant linkages.
83
+ current_child_includes += (results[key] && results[key][:include_linkages]) || []
84
+ current_child_includes.uniq!
85
+ results[key] = { object: obj, include_linkages: current_child_includes }
86
+ end
87
+ end
88
+
89
+ # Recurse deeper!
90
+ next if child_inclusion_tree.empty?
91
+
92
+ # For each object we just loaded, find all deeper recursive relationships.
93
+ objects.each do |obj|
94
+ find_recursive_relationships(obj, child_inclusion_tree, results, options)
95
+ end
96
+ end
97
+ nil
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,100 @@
1
+ require 'ipaddress'
2
+ require 'net/http'
3
+
4
+ module ForestAdminAgent
5
+ module Services
6
+ class IpWhitelist
7
+ RULE_MATCH_IP = 0
8
+ RULE_MATCH_RANGE = 1
9
+ RULE_MATCH_SUBNET = 2
10
+
11
+ def initialize
12
+ fetch_rules
13
+ end
14
+
15
+ def use_ip_whitelist
16
+ @use_ip_whitelist ||= false
17
+ end
18
+
19
+ def rules
20
+ @rules ||= []
21
+ end
22
+
23
+ def enabled?
24
+ use_ip_whitelist && !rules.empty?
25
+ end
26
+
27
+ def ip_matches_any_rule?(ip)
28
+ rules.any? { |rule| ip_matches_rule?(ip, rule) }
29
+ end
30
+
31
+ def ip_matches_rule?(ip, rule)
32
+ case rule['type']
33
+ when RULE_MATCH_IP
34
+ ip_match_ip?(ip, rule['ip'])
35
+ when RULE_MATCH_RANGE
36
+ ip_match_range?(ip, rule['ipMinimum'], rule['ipMaximum'])
37
+ when RULE_MATCH_SUBNET
38
+ ip_match_subnet?(ip, rule['range'])
39
+ else
40
+ raise 'Invalid rule type'
41
+ end
42
+ end
43
+
44
+ def ip_match_ip?(ip1, ip2)
45
+ return both_loopback?(ip1, ip2) unless same_ip_version?(ip1, ip2)
46
+
47
+ if ip1 == ip2
48
+ true
49
+ else
50
+ both_loopback?(ip1, ip2)
51
+ end
52
+ end
53
+
54
+ def same_ip_version?(ip1, ip2)
55
+ ip_version(ip1) == ip_version(ip2)
56
+ end
57
+
58
+ def ip_version(ip)
59
+ (IPAddress ip).is_a?(IPAddress::IPv4) ? :ip_v4 : :ip_v6
60
+ end
61
+
62
+ def both_loopback?(ip1, ip2)
63
+ IPAddress(ip1).loopback? && IPAddress(ip2).loopback?
64
+ end
65
+
66
+ def ip_match_range?(ip, min, max)
67
+ return false unless same_ip_version?(ip, min)
68
+
69
+ ip_range_minimum = (IPAddress min)
70
+ ip_range_maximum = (IPAddress max)
71
+ ip_value = (IPAddress ip)
72
+
73
+ ip_value >= ip_range_minimum && ip_value <= ip_range_maximum
74
+ end
75
+
76
+ def ip_match_subnet?(ip, subnet)
77
+ return false unless same_ip_version?(ip, subnet)
78
+
79
+ IPAddress(subnet).include?(IPAddress(ip))
80
+ end
81
+
82
+ private
83
+
84
+ def fetch_rules
85
+ response = Net::HTTP.get_response(
86
+ URI("#{Facades::Container.cache(:forest_server_url)}/liana/v1/ip-whitelist-rules"),
87
+ { 'Content-Type' => 'application/json', 'forest-secret-key' => Facades::Container.cache(:env_secret) }
88
+ )
89
+
90
+ raise Error, ForestAdminAgent::Utils::ErrorMessages::UNEXPECTED unless response.is_a?(Net::HTTPSuccess)
91
+
92
+ body = JSON.parse(response.body)
93
+ ip_whitelist_data = body['data']['attributes']
94
+
95
+ @use_ip_whitelist = ip_whitelist_data['use_ip_whitelist']
96
+ @rules = ip_whitelist_data['rules']
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,20 @@
1
+ require 'mono_logger'
2
+
3
+ module ForestAdminAgent
4
+ module Services
5
+ class LoggerService
6
+ attr_reader :default_logger
7
+
8
+ def initialize(logger_level = 'Info', logger = nil)
9
+ @logger_level = logger_level
10
+ @logger = logger
11
+ @default_logger = MonoLogger.new('forest_admin')
12
+ # TODO: HANDLE FORMATTER
13
+ end
14
+
15
+ def levels
16
+ %w[Debug Info Warn Error]
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,57 @@
1
+ require 'jwt'
2
+ require 'active_support/time'
3
+
4
+ module ForestAdminAgent
5
+ module Utils
6
+ class ConditionTreeParser
7
+ include ForestAdminDatasourceToolkit::Exceptions
8
+ include ForestAdminDatasourceToolkit::Utils
9
+ include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
10
+ include ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes
11
+
12
+ def self.from_plain_object(collection, filters)
13
+ if leaf?(filters)
14
+ operator = filters['operator'].titleize.tr(' ', '_')
15
+ value = parse_value(collection, filters)
16
+
17
+ return ConditionTreeLeaf.new(filters['field'], operator, value)
18
+ end
19
+
20
+ if branch?(filters)
21
+ aggregator = filters['aggregator'].capitalize
22
+ conditions = []
23
+ filters['conditions'].each do |sub_tree|
24
+ conditions << from_plain_object(collection, sub_tree)
25
+ end
26
+
27
+ return conditions.size == 1 ? conditions[0] : ConditionTreeBranch.new(aggregator, conditions)
28
+ end
29
+
30
+ raise ForestException, 'Failed to instantiate condition tree'
31
+ end
32
+
33
+ def self.parse_value(collection, leaf)
34
+ schema = Collection.get_field_schema(collection, leaf['field'])
35
+ operator = leaf['operator'].titleize.tr(' ', '_')
36
+
37
+ if operator == Operators::IN && leaf['field'].is_a?(String)
38
+ values = leaf['value'].split(',').map(&:strip)
39
+
40
+ return values.map { |item| !%w[false 0 no].include?(item) } if schema.column_type == 'Boolean'
41
+
42
+ return values.map(&:to_f).select { |item| item.is_a? Numeric } if schema.column_type == 'Number'
43
+ end
44
+
45
+ leaf['value']
46
+ end
47
+
48
+ def self.leaf?(filters)
49
+ filters.key?('field') && filters.key?('operator')
50
+ end
51
+
52
+ def self.branch?(filters)
53
+ filters.key?('aggregator') && filters.key?('conditions')
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,38 @@
1
+ module ForestAdminAgent
2
+ module Utils
3
+ class ErrorMessages
4
+ AUTH_SECRET_MISSING = 'Your Forest authSecret seems to be missing. Can you check that you properly set a Forest
5
+ authSecret in the Forest initializer?'.freeze
6
+
7
+ SECRET_AND_RENDERINGID_INCONSISTENT = 'Cannot retrieve the project you\'re trying to unlock. The envSecret and
8
+ renderingId seems to be missing or inconsistent.'.freeze
9
+
10
+ SERVER_DOWN = 'Cannot retrieve the data from the Forest server. Forest API seems to be down right now.'.freeze
11
+
12
+ SECRET_NOT_FOUND = 'Cannot retrieve the data from the Forest server. Can you check that you properly copied the
13
+ Forest envSecret in the Liana initializer?'.freeze
14
+
15
+ UNEXPECTED = 'Cannot retrieve the data from the Forest server. An error occured in Forest API'.freeze
16
+
17
+ INVALID_STATE_MISSING = 'Invalid response from the authentication server: the state parameter is missing'.freeze
18
+
19
+ INVALID_STATE_FORMAT = 'Invalid response from the authentication server: the state parameter is not at the right
20
+ format'.freeze
21
+
22
+ INVALID_STATE_RENDERING_ID = 'Invalid response from the authentication server: the state does not contain a
23
+ renderingId'.freeze
24
+
25
+ MISSING_RENDERING_ID = 'Authentication request must contain a renderingId'.freeze
26
+
27
+ INVALID_RENDERING_ID = 'The parameter renderingId is not valid'.freeze
28
+
29
+ REGISTRATION_FAILED = 'The registration to the authentication API failed, response: '.freeze
30
+
31
+ OIDC_CONFIGURATION_RETRIEVAL_FAILED = 'Failed to retrieve the provider\'s configuration.'.freeze
32
+
33
+ TWO_FACTOR_AUTHENTICATION_REQUIRED = 'TwoFactorAuthenticationRequiredForbiddenError'.freeze
34
+
35
+ AUTHORIZATION_FAILED = 'Error while authorizing the user on Forest Admin'.freeze
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,48 @@
1
+ module ForestAdminAgent
2
+ module Utils
3
+ class Id
4
+ include ForestAdminDatasourceToolkit::Utils
5
+ include ForestAdminDatasourceToolkit
6
+ def self.unpack_id(collection, packed_id, with_key: false)
7
+ primary_keys = ForestAdminDatasourceToolkit::Utils::Schema.primary_keys(collection)
8
+ primary_key_values = packed_id.to_s.split('|')
9
+ if (nb_pks = primary_keys.size) != (nb_values = primary_key_values.size)
10
+ raise Exceptions::ForestException, "Expected #{nb_pks} primary keys, found #{nb_values}"
11
+ end
12
+
13
+ result = primary_keys.map.with_index do |pk_name, index|
14
+ field = collection.fields[pk_name]
15
+ value = primary_key_values[index]
16
+ casted_value = field.column_type == 'Number' ? value.to_i : value
17
+ # TODO: call FieldValidator::validateValue($value, $field, $castedValue);
18
+
19
+ [pk_name, casted_value]
20
+ end.to_h
21
+
22
+ with_key ? result : result.values
23
+ end
24
+
25
+ def self.unpack_ids(collection, packed_ids, with_key: false)
26
+ packed_ids.map { |item| unpack_id(collection, item, with_key: with_key) }
27
+ end
28
+
29
+ def self.parse_selection_ids(collection, params, with_key: false)
30
+ attributes = begin
31
+ params.dig('data', 'attributes')
32
+ rescue StandardError
33
+ nil
34
+ end
35
+
36
+ are_excluded = attributes&.key?('all_records') ? attributes['all_records'] : false
37
+ input_ids = attributes&.key?('ids') ? attributes['ids'] : params['data'].map { |item| item['id'] }
38
+ ids = unpack_ids(
39
+ collection,
40
+ are_excluded ? attributes['all_records_ids_excluded'] : input_ids,
41
+ with_key: with_key
42
+ )
43
+
44
+ { are_excluded: are_excluded, ids: ids }
45
+ end
46
+ end
47
+ end
48
+ end