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.
- 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
|