forest_admin_agent 1.0.0.pre.beta.21

Sign up to get free protection for your applications and to get access to all the features.
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