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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +5 -0
- data/README.md +35 -0
- data/Rakefile +10 -0
- data/forest_admin +1 -0
- data/forest_admin_agent.gemspec +48 -0
- data/lib/forest_admin_agent/auth/auth_manager.rb +50 -0
- data/lib/forest_admin_agent/auth/oauth2/forest_provider.rb +62 -0
- data/lib/forest_admin_agent/auth/oauth2/forest_resource_owner.rb +42 -0
- data/lib/forest_admin_agent/auth/oauth2/oidc_config.rb +29 -0
- data/lib/forest_admin_agent/auth/oidc_client_manager.rb +71 -0
- data/lib/forest_admin_agent/builder/agent_factory.rb +77 -0
- data/lib/forest_admin_agent/facades/container.rb +23 -0
- data/lib/forest_admin_agent/facades/whitelist.rb +13 -0
- data/lib/forest_admin_agent/http/Exceptions/authentication_open_id_client.rb +16 -0
- data/lib/forest_admin_agent/http/Exceptions/http_exception.rb +17 -0
- data/lib/forest_admin_agent/http/Exceptions/not_found_error.rb +15 -0
- data/lib/forest_admin_agent/http/forest_admin_api_requester.rb +28 -0
- data/lib/forest_admin_agent/http/router.rb +52 -0
- data/lib/forest_admin_agent/routes/abstract_authenticated_route.rb +25 -0
- data/lib/forest_admin_agent/routes/abstract_related_route.rb +12 -0
- data/lib/forest_admin_agent/routes/abstract_route.rb +27 -0
- data/lib/forest_admin_agent/routes/resources/count.rb +41 -0
- data/lib/forest_admin_agent/routes/resources/delete.rb +51 -0
- data/lib/forest_admin_agent/routes/resources/list.rb +38 -0
- data/lib/forest_admin_agent/routes/resources/related/associate_related.rb +63 -0
- data/lib/forest_admin_agent/routes/resources/related/count_related.rb +56 -0
- data/lib/forest_admin_agent/routes/resources/related/dissociate_related.rb +97 -0
- data/lib/forest_admin_agent/routes/resources/related/list_related.rb +54 -0
- data/lib/forest_admin_agent/routes/resources/related/update_related.rb +102 -0
- data/lib/forest_admin_agent/routes/resources/show.rb +44 -0
- data/lib/forest_admin_agent/routes/resources/store.rb +51 -0
- data/lib/forest_admin_agent/routes/resources/update.rb +42 -0
- data/lib/forest_admin_agent/routes/security/authentication.rb +95 -0
- data/lib/forest_admin_agent/routes/system/health_check.rb +22 -0
- data/lib/forest_admin_agent/serializer/forest_serializer.rb +176 -0
- data/lib/forest_admin_agent/serializer/forest_serializer_override.rb +103 -0
- data/lib/forest_admin_agent/services/ip_whitelist.rb +100 -0
- data/lib/forest_admin_agent/services/logger_service.rb +20 -0
- data/lib/forest_admin_agent/utils/condition_tree_parser.rb +57 -0
- data/lib/forest_admin_agent/utils/error_messages.rb +38 -0
- data/lib/forest_admin_agent/utils/id.rb +48 -0
- data/lib/forest_admin_agent/utils/query_string_parser.rb +89 -0
- data/lib/forest_admin_agent/utils/schema/frontend_filterable.rb +73 -0
- data/lib/forest_admin_agent/utils/schema/generator_collection.rb +35 -0
- data/lib/forest_admin_agent/utils/schema/generator_field.rb +183 -0
- data/lib/forest_admin_agent/utils/schema/schema_emitter.rb +103 -0
- data/lib/forest_admin_agent/version.rb +3 -0
- data/lib/forest_admin_agent.rb +11 -0
- data/sig/forest_admin_agent/auth/auth_manager.rbs +14 -0
- data/sig/forest_admin_agent/auth/oauth2/forest_provider.rbs +16 -0
- data/sig/forest_admin_agent/auth/oauth2/forest_resource_owner.rbs +15 -0
- data/sig/forest_admin_agent/auth/oidc_client_manager.rbs +15 -0
- data/sig/forest_admin_agent/builder/agent_factory.rbs +21 -0
- data/sig/forest_admin_agent/facades/container.rbs +9 -0
- data/sig/forest_admin_agent/http/router.rbs +9 -0
- data/sig/forest_admin_agent/routes/abstract_route.rbs +12 -0
- data/sig/forest_admin_agent/routes/security/authentication.rbs +14 -0
- data/sig/forest_admin_agent/routes/system/health_check.rbs +10 -0
- data/sig/forest_admin_agent.rbs +4 -0
- 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
|