forest_admin_agent 1.0.0.pre.beta.21 → 1.0.0.pre.beta.22

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/forest_admin_agent.gemspec +2 -1
  3. data/lib/forest_admin_agent/auth/oauth2/forest_provider.rb +1 -1
  4. data/lib/forest_admin_agent/auth/oidc_client_manager.rb +3 -2
  5. data/lib/forest_admin_agent/builder/agent_factory.rb +7 -7
  6. data/lib/forest_admin_agent/http/Exceptions/authentication_open_id_client.rb +1 -1
  7. data/lib/forest_admin_agent/http/Exceptions/conflict_error.rb +14 -0
  8. data/lib/forest_admin_agent/http/Exceptions/forbidden_error.rb +14 -0
  9. data/lib/forest_admin_agent/http/Exceptions/not_found_error.rb +1 -1
  10. data/lib/forest_admin_agent/http/Exceptions/require_approval.rb +15 -0
  11. data/lib/forest_admin_agent/http/forest_admin_api_requester.rb +32 -4
  12. data/lib/forest_admin_agent/routes/abstract_authenticated_route.rb +1 -0
  13. data/lib/forest_admin_agent/routes/resources/count.rb +7 -4
  14. data/lib/forest_admin_agent/routes/resources/delete.rb +5 -1
  15. data/lib/forest_admin_agent/routes/resources/list.rb +9 -3
  16. data/lib/forest_admin_agent/routes/resources/related/associate_related.rb +10 -1
  17. data/lib/forest_admin_agent/routes/resources/related/count_related.rb +2 -1
  18. data/lib/forest_admin_agent/routes/resources/related/dissociate_related.rb +2 -0
  19. data/lib/forest_admin_agent/routes/resources/related/list_related.rb +9 -1
  20. data/lib/forest_admin_agent/routes/resources/related/update_related.rb +8 -1
  21. data/lib/forest_admin_agent/routes/resources/show.rb +5 -3
  22. data/lib/forest_admin_agent/routes/resources/store.rb +1 -0
  23. data/lib/forest_admin_agent/routes/resources/update.rb +4 -3
  24. data/lib/forest_admin_agent/services/permissions.rb +264 -0
  25. data/lib/forest_admin_agent/services/smart_action_checker.rb +95 -0
  26. data/lib/forest_admin_agent/services/sse_cache_invalidation.rb +45 -0
  27. data/lib/forest_admin_agent/utils/context_variables.rb +39 -0
  28. data/lib/forest_admin_agent/utils/context_variables_injector.rb +53 -0
  29. data/lib/forest_admin_agent/utils/schema/schema_emitter.rb +1 -1
  30. data/lib/forest_admin_agent/version.rb +1 -1
  31. data/lib/forest_admin_agent.rb +1 -0
  32. metadata +27 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a86f151f54daa3a9a8f46251c380448eed7c0fdf90ff82818a37fa213a868557
4
- data.tar.gz: 2af48bff7f6a6691f2fe495509dba2669ec64d9197886167a1de316c24846263
3
+ metadata.gz: b5714273d6e7144f00eb531b7a93d0e908698bd1dac7984dc47affd15a7514f6
4
+ data.tar.gz: 3b47af0daae4bc4e02b944f0d21be13aa18fe5e7abb9bfcfa03f2ea3272028bd
5
5
  SHA512:
6
- metadata.gz: 006cdc9ffa0c0afcb5ae7efb9a401b5964d379834bbf416d9f5cd7400de8425c83867ae2e0a83cd5a6f7d9de4043c018c3f2a650cab00383a576baf364ebaf22
7
- data.tar.gz: 3d04663fff62e53935d54c3a9312d5c575ff5b78ce7bd5fc2be1e73021f7dbda7ac43fe5f30dc141f07f6d9fb98cb23f54c093be65b2c8cb866daabfd8f97fcf
6
+ metadata.gz: 93044527fdfb1a06f49408044479258b1cccb6af15c8025cf26310f01c684ae6b5c4657bd25496eb69d911eaa77daae83ec82ec551469bd15cacea61a3b46687
7
+ data.tar.gz: f93863c6d28342854e2e453795bca3c3892c6a09c0d91a44fcb42fae57b107c17f6daa3e9694d04e0862bb46e1f95ff2d14f04c199ab0c2a5436434ca5090ca4
@@ -36,10 +36,11 @@ admin work on any Ruby application."
36
36
  spec.add_dependency "activesupport", ">= 6.1"
37
37
  spec.add_dependency "dry-container", "~> 0.11"
38
38
  spec.add_dependency "faraday", "~> 2.7"
39
+ spec.add_dependency "filecache", "~> 1.0"
39
40
  spec.add_dependency "ipaddress", "~> 0.8.3"
40
41
  spec.add_dependency "jsonapi-serializers", "~> 1.0"
41
42
  spec.add_dependency "jwt", "~> 2.7"
42
- spec.add_dependency "lightly", "~> 0.4.0"
43
+ spec.add_dependency "ld-eventsource", "~> 2.2"
43
44
  spec.add_dependency "mono_logger", "~> 1.1"
44
45
  spec.add_dependency "openid_connect", "~> 2.2"
45
46
  spec.add_dependency "rake", "~> 13.0"
@@ -8,7 +8,7 @@ module ForestAdminAgent
8
8
  attr_reader :rendering_id
9
9
 
10
10
  def initialize(rendering_id, attributes = {})
11
- super attributes
11
+ super(attributes)
12
12
  @rendering_id = rendering_id
13
13
  @authorization_endpoint = '/oidc/auth'
14
14
  @token_endpoint = '/oidc/token'
@@ -1,3 +1,4 @@
1
+ require 'filecache'
1
2
  require 'openid_connect'
2
3
  require_relative 'oauth2/oidc_config'
3
4
  require_relative 'oauth2/forest_provider'
@@ -18,8 +19,8 @@ module ForestAdminAgent
18
19
  private
19
20
 
20
21
  def setup_cache(env_secret, config_agent)
21
- lightly = Lightly.new(life: TTL, dir: "#{config_agent[:cache_dir]}/issuer")
22
- lightly.get env_secret do
22
+ cache = FileCache.new('auth_issuer', (config_agent[:cache_dir]).to_s, TTL)
23
+ cache.get_or_set env_secret do
23
24
  oidc_config = retrieve_config(config_agent[:forest_server_url])
24
25
  credentials = register(
25
26
  config_agent[:env_secret],
@@ -1,5 +1,5 @@
1
1
  require 'dry-container'
2
- require 'lightly'
2
+ require 'filecache'
3
3
 
4
4
  module ForestAdminAgent
5
5
  module Builder
@@ -21,7 +21,7 @@ module ForestAdminAgent
21
21
  end
22
22
 
23
23
  def add_datasource(datasource)
24
- datasource.collections.each { |_name, collection| @customizer.add_collection(collection) }
24
+ datasource.collections.each_value { |collection| @customizer.add_collection(collection) }
25
25
  self
26
26
  end
27
27
 
@@ -40,9 +40,9 @@ module ForestAdminAgent
40
40
  if !schema_is_know || force
41
41
  # Logger::log('Info', 'schema was updated, sending new version');
42
42
  client = ForestAdminAgent::Http::ForestAdminApiRequester.new
43
- client.post('/forest/apimaps', schema)
44
- schema_file_hash_cache = Lightly.new(life: TTL_SCHEMA, dir: @options[:cache_dir].to_s)
45
- schema_file_hash_cache.get 'value' do
43
+ client.post('/forest/apimaps', schema.to_json)
44
+ schema_file_hash_cache = FileCache.new('app', @options[:cache_dir].to_s, TTL_SCHEMA)
45
+ schema_file_hash_cache.get_or_set 'value' do
46
46
  schema[:meta][:schemaFileHash]
47
47
  end
48
48
  @container.register(:schema_file_hash, schema_file_hash_cache)
@@ -59,11 +59,11 @@ module ForestAdminAgent
59
59
  end
60
60
 
61
61
  def build_cache
62
- @container.register(:cache, Lightly.new(life: TTL_CONFIG, dir: @options[:cache_dir].to_s))
62
+ @container.register(:cache, FileCache.new('app', @options[:cache_dir].to_s, TTL_SCHEMA))
63
63
  return unless @has_env_secret
64
64
 
65
65
  cache = @container.resolve(:cache)
66
- cache.get 'config' do
66
+ cache.get_or_set 'config' do
67
67
  @options.to_h
68
68
  end
69
69
  end
@@ -5,7 +5,7 @@ module ForestAdminAgent
5
5
  attr_reader :error, :error_description, :state
6
6
 
7
7
  def initialize(error, error_description, state)
8
- super error, 401, error_description
8
+ super(error, 401, error_description)
9
9
  @error = error
10
10
  @error_description = error_description
11
11
  @state = state
@@ -0,0 +1,14 @@
1
+ module ForestAdminAgent
2
+ module Http
3
+ module Exceptions
4
+ class ConflictError < HttpException
5
+ attr_reader :name
6
+
7
+ def initialize(message, name = 'ConflictError')
8
+ @name = name
9
+ super(429, 'Conflict', message)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module ForestAdminAgent
2
+ module Http
3
+ module Exceptions
4
+ class ForbiddenError < HttpException
5
+ attr_reader :name
6
+
7
+ def initialize(message, name = 'ForbiddenError')
8
+ @name = name
9
+ super(403, 'Forbidden', message)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -5,7 +5,7 @@ module ForestAdminAgent
5
5
  attr_reader :name, :status
6
6
 
7
7
  def initialize(msg, name = 'NotFoundError')
8
- super msg
8
+ super(msg)
9
9
  @name = name
10
10
  @status = 404
11
11
  end
@@ -0,0 +1,15 @@
1
+ module ForestAdminAgent
2
+ module Http
3
+ module Exceptions
4
+ class RequireApproval < HttpException
5
+ attr_reader :name, :data
6
+
7
+ def initialize(message, name = 'RequireApproval', data = [])
8
+ @name = name
9
+ @data = data
10
+ super(403, 'Forbidden', message)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -3,6 +3,8 @@ require 'faraday'
3
3
  module ForestAdminAgent
4
4
  module Http
5
5
  class ForestAdminApiRequester
6
+ include ForestAdminDatasourceToolkit::Exceptions
7
+
6
8
  def initialize
7
9
  @headers = {
8
10
  'Content-Type' => 'application/json',
@@ -16,12 +18,38 @@ module ForestAdminAgent
16
18
  )
17
19
  end
18
20
 
19
- def get(url, params)
20
- @client.get(url, params.to_json)
21
+ def get(url, params = nil)
22
+ @client.get(url, params)
23
+ end
24
+
25
+ def post(url, params = nil)
26
+ @client.post(url, params)
21
27
  end
22
28
 
23
- def post(url, params)
24
- @client.post(url, params.to_json)
29
+ def handle_response_error(error)
30
+ raise error if error.is_a?(ForestException)
31
+
32
+ if error.response[:message]&.include?('certificate')
33
+ raise ForestException,
34
+ 'ForestAdmin server TLS certificate cannot be verified. Please check that your system time is set properly.'
35
+ end
36
+
37
+ if error.response[:status].zero? || error.response[:status] == 502
38
+ raise ForestException, 'Failed to reach ForestAdmin server. Are you online?'
39
+ end
40
+
41
+ if error.response[:status] == 404
42
+ raise ForestException,
43
+ 'ForestAdmin server failed to find the project related to the envSecret you configured. Can you check that you copied it properly in the Forest initialization?'
44
+ end
45
+
46
+ if error.response[:status] == 503
47
+ raise ForestException,
48
+ 'Forest is in maintenance for a few minutes. We are upgrading your experience in the forest. We just need a few more minutes to get it right.'
49
+ end
50
+
51
+ raise ForestException,
52
+ 'An unexpected error occurred while contacting the ForestAdmin server. Please contact support@forestadmin.com for further investigations.'
25
53
  end
26
54
  end
27
55
  end
@@ -6,6 +6,7 @@ module ForestAdminAgent
6
6
  Facades::Whitelist.check_ip(args[:headers]['action_dispatch.remote_ip'].to_s)
7
7
  end
8
8
  @caller = Utils::QueryStringParser.parse_caller(args)
9
+ @permissions = ForestAdminAgent::Services::Permissions.new(@caller)
9
10
  super
10
11
  end
11
12
 
@@ -3,8 +3,9 @@ require 'jsonapi-serializers'
3
3
  module ForestAdminAgent
4
4
  module Routes
5
5
  module Resources
6
- class Count < AbstractRoute
6
+ class Count < AbstractAuthenticatedRoute
7
7
  include ForestAdminAgent::Builder
8
+ include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
8
9
  def setup_routes
9
10
  add_route('forest_count', 'get', '/:collection_name/count', ->(args) { handle_request(args) })
10
11
 
@@ -13,12 +14,14 @@ module ForestAdminAgent
13
14
 
14
15
  def handle_request(args = {})
15
16
  build(args)
17
+ @permissions.can?(:browse, @collection)
16
18
 
17
19
  if @collection.is_countable?
18
- caller = ForestAdminAgent::Utils::QueryStringParser.parse_caller(args)
19
- filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new
20
+ filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
21
+ condition_tree: @permissions.get_scope(@collection)
22
+ )
20
23
  aggregation = ForestAdminDatasourceToolkit::Components::Query::Aggregation.new(operation: 'Count')
21
- result = @collection.aggregate(caller, filter, aggregation)
24
+ result = @collection.aggregate(@caller, filter, aggregation)
22
25
 
23
26
  return {
24
27
  name: args[:params]['collection_name'],
@@ -7,6 +7,7 @@ module ForestAdminAgent
7
7
  class Delete < AbstractAuthenticatedRoute
8
8
  include ForestAdminAgent::Builder
9
9
  include ForestAdminDatasourceToolkit::Components::Query
10
+ include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
10
11
 
11
12
  def setup_routes
12
13
  add_route('forest_delete_bulk', 'delete', '/:collection_name', ->(args) { handle_request_bulk(args) })
@@ -17,6 +18,7 @@ module ForestAdminAgent
17
18
 
18
19
  def handle_request(args = {})
19
20
  build(args)
21
+ @permissions.can?(:delete, @collection)
20
22
  id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true)
21
23
  delete_records(args, { ids: [id], are_excluded: false })
22
24
 
@@ -25,6 +27,7 @@ module ForestAdminAgent
25
27
 
26
28
  def handle_request_bulk(args = {})
27
29
  build(args)
30
+ @permissions.can?(:delete, @collection)
28
31
  selection_ids = Utils::Id.parse_selection_ids(@collection, args[:params], with_key: true)
29
32
  delete_records(args, selection_ids)
30
33
 
@@ -38,7 +41,8 @@ module ForestAdminAgent
38
41
  condition_tree: ConditionTree::ConditionTreeFactory.intersect(
39
42
  [
40
43
  Utils::QueryStringParser.parse_condition_tree(@collection, args),
41
- condition_tree_ids
44
+ condition_tree_ids,
45
+ @permissions.get_scope(@collection)
42
46
  ]
43
47
  )
44
48
  )
@@ -5,6 +5,7 @@ module ForestAdminAgent
5
5
  module Resources
6
6
  class List < AbstractAuthenticatedRoute
7
7
  include ForestAdminAgent::Builder
8
+ include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
8
9
  def setup_routes
9
10
  add_route('forest_list', 'get', '/:collection_name', ->(args) { handle_request(args) })
10
11
 
@@ -13,14 +14,19 @@ module ForestAdminAgent
13
14
 
14
15
  def handle_request(args = {})
15
16
  build(args)
16
- caller = ForestAdminAgent::Utils::QueryStringParser.parse_caller(args)
17
+ @permissions.can?(:browse, @collection)
17
18
 
18
19
  filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
19
- condition_tree: ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(@collection, args),
20
+ condition_tree: ConditionTreeFactory.intersect([
21
+ @permissions.get_scope(@collection),
22
+ ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
23
+ @collection, args
24
+ )
25
+ ]),
20
26
  page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args)
21
27
  )
22
28
  projection = ForestAdminAgent::Utils::QueryStringParser.parse_projection_with_pks(@collection, args)
23
- records = @collection.list(caller, filter, projection)
29
+ records = @collection.list(@caller, filter, projection)
24
30
 
25
31
  {
26
32
  name: args[:params]['collection_name'],
@@ -8,6 +8,7 @@ module ForestAdminAgent
8
8
  include ForestAdminAgent::Builder
9
9
  include ForestAdminDatasourceToolkit::Utils
10
10
  include ForestAdminDatasourceToolkit::Components::Query
11
+
11
12
  def setup_routes
12
13
  add_route(
13
14
  'forest_related_associate',
@@ -21,6 +22,7 @@ module ForestAdminAgent
21
22
 
22
23
  def handle_request(args = {})
23
24
  build(args)
25
+ @permissions.can?(:edit, @collection)
24
26
 
25
27
  parent_id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true)
26
28
  target_relation_id = Utils::Id.unpack_id(@child_collection, args[:params]['data'][0]['id'], with_key: true)
@@ -40,7 +42,14 @@ module ForestAdminAgent
40
42
  def associate_one_to_many(relation, parent_id, target_relation_id)
41
43
  id = Schema.primary_keys(@child_collection)[0]
42
44
  value = Collection.get_value(@child_collection, @caller, target_relation_id, id)
43
- filter = Filter.new(condition_tree: ConditionTree::Nodes::ConditionTreeLeaf.new(id, 'Equal', value))
45
+ filter = Filter.new(
46
+ condition_tree: ConditionTree::ConditionTreeFactory.intersect(
47
+ [
48
+ ConditionTree::Nodes::ConditionTreeLeaf.new(id, 'Equal', value),
49
+ @permissions.get_scope(@collection)
50
+ ]
51
+ )
52
+ )
44
53
  value = Collection.get_value(@collection, @caller, parent_id, relation.origin_key_target)
45
54
 
46
55
  @child_collection.update(@caller, filter, { relation.origin_key => value })
@@ -21,9 +21,10 @@ module ForestAdminAgent
21
21
 
22
22
  def handle_request(args = {})
23
23
  build(args)
24
+ @permissions.can?(:browse, @collection)
24
25
 
25
26
  if @child_collection.is_countable?
26
- filter = Filter.new
27
+ filter = Filter.new(condition_tree: @permissions.get_scope(@collection))
27
28
  id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true)
28
29
  result = Collection.aggregate_relation(
29
30
  @collection,
@@ -21,6 +21,7 @@ module ForestAdminAgent
21
21
 
22
22
  def handle_request(args = {})
23
23
  build(args)
24
+ @permissions.can?(:delete, @collection)
24
25
 
25
26
  parent_id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true)
26
27
  is_delete_mode = !args.dig(:params, :delete).nil?
@@ -84,6 +85,7 @@ module ForestAdminAgent
84
85
  Filter.new(
85
86
  condition_tree: ConditionTree::ConditionTreeFactory.intersect(
86
87
  [
88
+ @permissions.get_scope(@child_collection),
87
89
  Utils::QueryStringParser.parse_condition_tree(@child_collection, args),
88
90
  selected_ids
89
91
  ]
@@ -7,6 +7,8 @@ module ForestAdminAgent
7
7
  class ListRelated < AbstractRelatedRoute
8
8
  include ForestAdminAgent::Builder
9
9
  include ForestAdminDatasourceToolkit::Utils
10
+ include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
11
+
10
12
  def setup_routes
11
13
  add_route(
12
14
  'forest_related_list',
@@ -20,10 +22,16 @@ module ForestAdminAgent
20
22
 
21
23
  def handle_request(args = {})
22
24
  build(args)
25
+ @permissions.can?(:browse, @collection)
23
26
  # TODO: add csv behaviour
24
27
 
25
28
  filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
26
- condition_tree: ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(@child_collection, args),
29
+ condition_tree: ConditionTreeFactory.intersect(
30
+ [
31
+ @permissions.get_scope(@collection),
32
+ ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(@child_collection, args)
33
+ ]
34
+ ),
27
35
  page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args)
28
36
  )
29
37
  projection = ForestAdminAgent::Utils::QueryStringParser.parse_projection_with_pks(@child_collection, args)
@@ -21,6 +21,7 @@ module ForestAdminAgent
21
21
 
22
22
  def handle_request(args = {})
23
23
  build(args)
24
+ @permissions.can?(:edit, @collection)
24
25
 
25
26
  relation = @collection.fields[args[:params]['relation_name']]
26
27
  parent_id = Utils::Id.unpack_id(@collection, args[:params]['id'])
@@ -61,6 +62,7 @@ module ForestAdminAgent
61
62
  old_fk_owner_filter = Filter.new(
62
63
  condition_tree: ConditionTree::ConditionTreeFactory.intersect(
63
64
  [
65
+ @permissions.get_scope(@collection),
64
66
  ConditionTree::Nodes::ConditionTreeLeaf.new(
65
67
  relation.origin_key,
66
68
  ConditionTree::Operators::EQUAL,
@@ -91,7 +93,12 @@ module ForestAdminAgent
91
93
 
92
94
  @child_collection.update(
93
95
  @caller,
94
- Filter.new(condition_tree: new_fk_owner),
96
+ Filter.new(condition_tree: ConditionTree::ConditionTreeFactory.intersect(
97
+ [
98
+ @permissions.get_scope(@collection),
99
+ new_fk_owner
100
+ ]
101
+ )),
95
102
  { relation.origin_key => origin_value }
96
103
  )
97
104
  end
@@ -15,16 +15,18 @@ module ForestAdminAgent
15
15
 
16
16
  def handle_request(args = {})
17
17
  build(args)
18
+ @permissions.can?(:read, @collection)
19
+ scope = @permissions.get_scope(@collection)
18
20
  id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true)
19
- caller = ForestAdminAgent::Utils::QueryStringParser.parse_caller(args)
20
21
  condition_tree = ConditionTree::ConditionTreeFactory.match_records(@collection, [id])
21
22
  filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
22
- condition_tree: condition_tree,
23
+ condition_tree: ConditionTree::ConditionTreeFactory.intersect([condition_tree, scope]),
23
24
  page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args)
24
25
  )
26
+
25
27
  projection = ProjectionFactory.all(@collection)
26
28
 
27
- records = @collection.list(caller, filter, projection)
29
+ records = @collection.list(@caller, filter, projection)
28
30
 
29
31
  raise Http::Exceptions::NotFoundError, 'Record does not exists' unless records.size.positive?
30
32
 
@@ -15,6 +15,7 @@ module ForestAdminAgent
15
15
 
16
16
  def handle_request(args = {})
17
17
  build(args)
18
+ @permissions.can?(:add, @collection)
18
19
  data = format_attributes(args)
19
20
  record = @collection.create(@caller, data)
20
21
  link_one_to_one_relations(args, record)
@@ -16,16 +16,17 @@ module ForestAdminAgent
16
16
 
17
17
  def handle_request(args = {})
18
18
  build(args)
19
+ @permissions.can?(:edit, @collection)
20
+ scope = @permissions.get_scope(@collection)
19
21
  id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true)
20
- caller = ForestAdminAgent::Utils::QueryStringParser.parse_caller(args)
21
22
  condition_tree = ConditionTree::ConditionTreeFactory.match_records(@collection, [id])
22
23
  filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
23
- condition_tree: condition_tree,
24
+ condition_tree: ConditionTree::ConditionTreeFactory.intersect([condition_tree, scope]),
24
25
  page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args)
25
26
  )
26
27
  data = format_attributes(args)
27
28
  @collection.update(@caller, filter, data)
28
- records = @collection.list(caller, filter, ProjectionFactory.all(@collection))
29
+ records = @collection.list(@caller, filter, ProjectionFactory.all(@collection))
29
30
 
30
31
  {
31
32
  name: args[:params]['collection_name'],
@@ -0,0 +1,264 @@
1
+ require 'filecache'
2
+
3
+ module ForestAdminAgent
4
+ module Services
5
+ class Permissions
6
+ include ForestAdminAgent::Http::Exceptions
7
+ include ForestAdminAgent::Utils
8
+ include ForestAdminDatasourceToolkit::Exceptions
9
+ include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
10
+
11
+ attr_reader :caller, :forest_api, :cache
12
+
13
+ def initialize(caller)
14
+ @caller = caller
15
+ @forest_api = ForestAdminAgent::Http::ForestAdminApiRequester.new
16
+ @cache = FileCache.new(
17
+ 'permissions',
18
+ Facades::Container.config_from_cache[:cache_dir].to_s,
19
+ Facades::Container.config_from_cache[:permission_expiration]
20
+ )
21
+ end
22
+
23
+ def self.invalidate_cache(id_cache = nil)
24
+ cache = FileCache.new(
25
+ 'permissions',
26
+ Facades::Container.config_from_cache[:cache_dir].to_s,
27
+ Facades::Container.config_from_cache[:permission_expiration]
28
+ )
29
+
30
+ cache.clear if id_cache.nil?
31
+
32
+ cache.delete(id_cache) unless cache.get(id_cache).nil?
33
+
34
+ # TODO: HANDLE LOGGER
35
+ # logger.debug("Invalidating #{id_cache} cache..")
36
+ end
37
+
38
+ def can?(action, collection, allow_fetch: false)
39
+ return true unless permission_system?
40
+
41
+ user_data = get_user_data(caller.id)
42
+ collections_data = get_collections_permissions_data(force_fetch: allow_fetch)
43
+
44
+ is_allowed = collections_data.key?(collection.name.to_sym) && collections_data[collection.name.to_sym][action].include?(user_data[:roleId])
45
+
46
+ # Refetch
47
+ unless is_allowed
48
+ collections_data = get_collections_permissions_data(force_fetch: true)
49
+ is_allowed = collections_data[collection.name.to_sym][action].include?(user_data[:roleId])
50
+ end
51
+
52
+ # still not allowed - throw forbidden message
53
+ raise ForbiddenError, "You don't have permission to #{action} this collection." unless is_allowed
54
+
55
+ is_allowed
56
+ end
57
+
58
+ def can_chart?(parameters)
59
+ attributes = sanitize_chart_parameters(parameters)
60
+ hash_request = "#{attributes[:type]}:#{array_hash(attributes)}"
61
+ is_allowed = get_chart_data(caller.rendering_id).include?(hash_request)
62
+
63
+ # Refetch
64
+ is_allowed ||= get_chart_data(caller.rendering_id, force_fetch: true).include?(hash_request)
65
+
66
+ # still not allowed - throw forbidden message
67
+ unless is_allowed
68
+ # TODO: HANDLE LOGGER
69
+ # logger.debug("User #{caller.id} cannot retrieve chart on rendering #{caller.rendering_id}")
70
+ raise ForbiddenError, "You don't have permission to access this collection."
71
+ end
72
+
73
+ # TODO: HANDLE LOGGER
74
+ # logger.debug("User #{caller.id} can retrieve chart on rendering #{caller.rendering_id}")
75
+
76
+ is_allowed
77
+ end
78
+
79
+ def can_smart_action?(request, collection, filter, allow_fetch: true)
80
+ return true unless permission_system?
81
+
82
+ user_data = get_user_data(caller.id)
83
+ collections_data = get_collections_permissions_data(force_fetch: allow_fetch)
84
+ action = find_action_from_endpoint(collection.name, request[:headers]['REQUEST_PATH'], request[:headers]['REQUEST_METHOD'])
85
+ smart_action_approval = SmartActionChecker.new(
86
+ request[:params],
87
+ collection,
88
+ collections_data[collection.name.to_sym][:actions][action[:name]],
89
+ caller,
90
+ user_data[:roleId],
91
+ filter
92
+ )
93
+
94
+ smart_action_approval.can_execute?
95
+ # TODO: HANDLE LOGGER
96
+ # logger.debug("User #{user_data[:roleId]} is #{is_allowed ? '' : 'not'} allowed to perform #{action['name']}")
97
+ end
98
+
99
+ def get_scope(collection)
100
+ permissions = get_scope_and_team_data(caller.rendering_id)
101
+ scope = permissions[:scopes][collection.name.to_sym]
102
+ team = permissions[:team]
103
+ user = get_user_data(caller.id)
104
+
105
+ return nil if scope.nil?
106
+
107
+ context_variables = ContextVariables.new(team, user)
108
+
109
+ ContextVariablesInjector.inject_context_in_filter(scope, context_variables)
110
+ end
111
+
112
+ def get_user_data(user_id)
113
+ cache.get_or_set('forest.users') do
114
+ response = fetch('/liana/v4/permissions/users')
115
+ users = {}
116
+
117
+ response.each do |user|
118
+ users[user[:id].to_s] = user
119
+ end
120
+
121
+ # TODO: HANDLE LOGGER
122
+ # logger.debug('Refreshing user permissions cache')
123
+
124
+ users
125
+ end[user_id.to_s]
126
+ end
127
+
128
+ def get_team(rendering_id)
129
+ permissions = get_scope_and_team_data(rendering_id)
130
+
131
+ permissions[:team]
132
+ end
133
+
134
+ private
135
+
136
+ def get_collections_permissions_data(force_fetch: false)
137
+ self.class.invalidate_cache('forest.collections') if force_fetch == true
138
+
139
+ cache.get_or_set('forest.collections') do
140
+ response = fetch('/liana/v4/permissions/environment')
141
+ collections = {}
142
+
143
+ response[:collections].each do |name, collection|
144
+ collections[name] = decode_crud_permissions(collection).merge(decode_action_permissions(collection))
145
+ end
146
+
147
+ # TODO: HANDLE LOGGER
148
+ # logger.debug('Fetching environment permissions')
149
+
150
+ collections
151
+ end
152
+ end
153
+
154
+ def get_chart_data(rendering_id, force_fetch: false)
155
+ self.class.invalidate_cache('forest.stats') if force_fetch == true
156
+
157
+ cache.get_or_set('forest.stats') do
158
+ response = fetch("/liana/v4/permissions/renderings/#{rendering_id}")
159
+ stat_hash = []
160
+ response[:stats].each do |stat|
161
+ stat = stat.select { |_, value| !value.nil? && value != '' }
162
+ stat_hash << "#{stat[:type]}:#{array_hash(stat)}"
163
+ end
164
+
165
+ # TODO: HANDLE LOGGER
166
+ # logger.debug("Loading rendering permissions for rendering #{rendering_id}")
167
+
168
+ stat_hash
169
+ end
170
+ end
171
+
172
+ def sanitize_chart_parameters(parameters)
173
+ # parameters = parameters.to_h
174
+ parameters.delete(:timezone)
175
+ parameters.delete(:collection)
176
+ parameters.delete(:contextVariables)
177
+
178
+ parameters.select { |_, value| !value.nil? && value != '' }
179
+ end
180
+
181
+ def array_hash(data)
182
+ Digest::SHA1.hexdigest(data.sort.to_h.to_s)
183
+ end
184
+
185
+ def get_scope_and_team_data(rendering_id)
186
+ cache.get_or_set('forest.scopes') do
187
+ data = {}
188
+ response = fetch("/liana/v4/permissions/renderings/#{rendering_id}")
189
+
190
+ data[:scopes] = decode_scope_permissions(response[:collections])
191
+ data[:team] = response[:team]
192
+
193
+ data
194
+ end
195
+ end
196
+
197
+ def permission_system?
198
+ cache.get_or_set('forest.has_permission') do
199
+ response = fetch('/liana/v4/permissions/environment')
200
+ { enable: !response.nil? }
201
+ end[:enable]
202
+ end
203
+
204
+ def find_action_from_endpoint(collection_name, endpoint, http_method)
205
+ schema_file = JSON.parse(File.read(Facades::Container.config_from_cache[:schema_path]))
206
+ actions = schema_file['collections']&.select { |collection| collection['name'] == collection_name }&.first&.dig('actions')
207
+
208
+ return nil if actions.nil? || actions.empty?
209
+
210
+ action = actions.find { |a| a['endpoint'] == endpoint && a['http_method'].casecmp(http_method).zero? }
211
+
212
+ raise ForestException, "The collection #{collection_name} does not have this smart action" if action.nil?
213
+
214
+ action
215
+ end
216
+
217
+ def decode_crud_permissions(collection)
218
+ {
219
+ browse: collection[:collection][:browseEnabled][:roles],
220
+ read: collection[:collection][:readEnabled][:roles],
221
+ edit: collection[:collection][:editEnabled][:roles],
222
+ add: collection[:collection][:addEnabled][:roles],
223
+ delete: collection[:collection][:deleteEnabled][:roles],
224
+ export: collection[:collection][:exportEnabled][:roles]
225
+ }
226
+ end
227
+
228
+ def decode_action_permissions(collection)
229
+ actions = {}
230
+ actions[:actions] = {}
231
+ collection[:actions].each do |id, action|
232
+ actions[:actions][id] = {
233
+ triggerEnabled: action[:triggerEnabled][:roles],
234
+ triggerConditions: action[:triggerConditions],
235
+ approvalRequired: action[:approvalRequired][:roles],
236
+ approvalRequiredConditions: action[:approvalRequiredConditions],
237
+ userApprovalEnabled: action[:userApprovalEnabled][:roles],
238
+ userApprovalConditions: action[:userApprovalConditions],
239
+ selfApprovalEnabled: action[:selfApprovalEnabled][:roles]
240
+ }
241
+ end
242
+
243
+ actions
244
+ end
245
+
246
+ def decode_scope_permissions(raw_permissions)
247
+ scopes = {}
248
+ raw_permissions.each do |collection_name, value|
249
+ scopes[collection_name] = ConditionTreeFactory.from_plain_object(value[:scope]) unless value[:scope].nil?
250
+ end
251
+
252
+ scopes
253
+ end
254
+
255
+ def fetch(url)
256
+ response = forest_api.get(url)
257
+
258
+ JSON.parse(response.body, symbolize_names: true)
259
+ rescue StandardError => e
260
+ forest_api.handle_response_error(e)
261
+ end
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,95 @@
1
+ module ForestAdminAgent
2
+ module Services
3
+ class SmartActionChecker
4
+ include ForestAdminAgent::Http::Exceptions
5
+ include ForestAdminAgent::Utils
6
+ include ForestAdminDatasourceToolkit::Utils
7
+ include ForestAdminDatasourceToolkit::Components::Query
8
+ include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
9
+
10
+ attr_reader :parameters, :collection, :smart_action, :caller, :role_id, :filter, :attributes
11
+
12
+ TRIGGER_FORBIDDEN_ERROR = 'CustomActionTriggerForbiddenError'.freeze
13
+
14
+ REQUIRE_APPROVAL_ERROR = 'CustomActionRequiresApprovalError'.freeze
15
+
16
+ INVALID_ACTION_CONDITION_ERROR = 'InvalidActionConditionError'.freeze
17
+
18
+ def initialize(parameters, collection, smart_action, caller, role_id, filter)
19
+ @parameters = parameters
20
+ @collection = collection
21
+ @smart_action = smart_action
22
+ @caller = caller
23
+ @role_id = role_id
24
+ @filter = filter
25
+ @attributes = parameters[:data][:attributes]
26
+ end
27
+
28
+ def can_execute?
29
+ if attributes[:signed_approval_request].nil?
30
+ can_trigger?
31
+ else
32
+ can_approve?
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def can_approve?
39
+ if smart_action[:userApprovalEnabled].include?(role_id) &&
40
+ (smart_action[:userApprovalConditions].empty? || match_conditions(:userApprovalConditions)) &&
41
+ (attributes[:requester_id] != caller.id || smart_action[:selfApprovalEnabled].include?(role_id))
42
+ return true
43
+ end
44
+
45
+ raise ForbiddenError.new('You don\'t have the permission to trigger this action.', TRIGGER_FORBIDDEN_ERROR)
46
+ end
47
+
48
+ def can_trigger?
49
+ if smart_action[:triggerEnabled].include?(role_id) && !smart_action[:approvalRequired].include?(role_id)
50
+ return true if smart_action[:triggerConditions].empty? || match_conditions(:triggerConditions)
51
+ elsif smart_action[:approvalRequired].include?(role_id) && smart_action[:triggerEnabled].include?(role_id)
52
+ if smart_action[:approvalRequiredConditions].empty? || match_conditions(:approvalRequiredConditions)
53
+ raise RequireApproval.new(
54
+ 'This action requires to be approved.',
55
+ REQUIRE_APPROVAL_ERROR,
56
+ smart_action[:userApprovalEnabled]
57
+ )
58
+ elsif smart_action[:triggerConditions].empty? || match_conditions(:triggerConditions)
59
+ return true
60
+ end
61
+ end
62
+
63
+ raise ForbiddenError.new('You don\'t have the permission to trigger this action.', TRIGGER_FORBIDDEN_ERROR)
64
+ end
65
+
66
+ def match_conditions(condition_name)
67
+ pk = Schema.primary_keys(collection)[0]
68
+ condition_filter = if attributes[:all_records]
69
+ Nodes::ConditionTreeLeaf.new(pk, 'NOT_EQUAL', attributes[:all_records_ids_excluded])
70
+ else
71
+ Nodes::ConditionTreeLeaf.new(pk, 'IN', attributes[:ids])
72
+ end
73
+
74
+ condition = smart_action[condition_name][0]['filter']
75
+ conditional_filter = filter.override(
76
+ condition_tree: ConditionTreeFactory.intersect(
77
+ [
78
+ ConditionTreeParser.from_plain_object(collection, condition),
79
+ filter.condition_tree,
80
+ condition_filter
81
+ ]
82
+ )
83
+ )
84
+ rows = collection.aggregate(caller, conditional_filter, Aggregation.new(operation: 'Count'))
85
+
86
+ (rows[0]['value'] || 0) == attributes[:ids].count
87
+ rescue StandardError
88
+ raise ConflictError.new(
89
+ 'The conditions to trigger this action cannot be verified. Please contact an administrator.',
90
+ INVALID_ACTION_CONDITION_ERROR
91
+ )
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,45 @@
1
+ require 'ld-eventsource'
2
+
3
+ module ForestAdminAgent
4
+ module Services
5
+ class SSECacheInvalidation
6
+ include ForestAdminDatasourceToolkit::Exceptions
7
+
8
+ MESSAGE_CACHE_KEYS = {
9
+ 'refresh-users': %w[forest.users],
10
+ 'refresh-roles': %w[forest.collections],
11
+ 'refresh-renderings': %w[forest.collections forest.stats forest.scopes]
12
+ # TODO: add one for ip whitelist when server implement it
13
+ }.freeze
14
+
15
+ def self.run
16
+ uri = "#{Facades::Container.config_from_cache[:forest_server_url]}/liana/v4/subscribe-to-events"
17
+ headers = {
18
+ 'forest-secret-key' => Facades::Container.config_from_cache[:env_secret],
19
+ 'Accept' => 'text/event-stream'
20
+ }
21
+
22
+ begin
23
+ SSE::Client.new(uri, headers: headers) do |client|
24
+ client.on_event do |event|
25
+ next if event.type == :heartbeat
26
+
27
+ MESSAGE_CACHE_KEYS[event.type]&.each do |cache_key|
28
+ Permissions.invalidate_cache(cache_key)
29
+ # TODO: HANDLE LOGGER
30
+ # "info","invalidate cache {MESSAGE_CACHE_KEYS[event.type]} for event {event.type}"
31
+ end
32
+ # TODO: HANDLE LOGGER add else
33
+ # "info", "SSECacheInvalidation: unhandled message from server: {event.type}"
34
+ end
35
+ end
36
+ rescue StandardError
37
+ raise ForestException, 'Failed to reach SSE data from ForestAdmin server.'
38
+ # TODO: HANDLE LOGGER
39
+ # "debug", "SSE connection to forestadmin server due to ..."
40
+ # "warning", "SSE connection to forestadmin server closed unexpectedly, retrying."
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,39 @@
1
+ module ForestAdminAgent
2
+ module Utils
3
+ class ContextVariables
4
+ attr_reader :team, :user, :request_context_variables
5
+
6
+ USER_VALUE_PREFIX = 'currentUser.'.freeze
7
+
8
+ USER_VALUE_TAG_PREFIX = 'currentUser.tags.'.freeze
9
+
10
+ USER_VALUE_TEAM_PREFIX = 'currentUser.team.'.freeze
11
+
12
+ def initialize(team, user, request_context_variables = nil)
13
+ @team = team.transform_keys(&:to_sym)
14
+ @user = user.transform_keys(&:to_sym)
15
+ @request_context_variables = request_context_variables
16
+ end
17
+
18
+ def get_value(context_variable_key)
19
+ return get_current_user_data(context_variable_key) if context_variable_key.start_with?(USER_VALUE_PREFIX)
20
+
21
+ request_context_variables[context_variable_key]
22
+ end
23
+
24
+ private
25
+
26
+ def get_current_user_data(context_variable_key)
27
+ if context_variable_key.start_with?(USER_VALUE_TEAM_PREFIX)
28
+ return team[context_variable_key[USER_VALUE_TEAM_PREFIX.length..].to_sym]
29
+ end
30
+
31
+ if context_variable_key.start_with?(USER_VALUE_TAG_PREFIX)
32
+ return user[:tags][context_variable_key[USER_VALUE_TAG_PREFIX.length..]]
33
+ end
34
+
35
+ user[context_variable_key[USER_VALUE_PREFIX.length..].to_sym]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,53 @@
1
+ module ForestAdminAgent
2
+ module Utils
3
+ class ContextVariablesInjector
4
+ include ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes
5
+
6
+ def self.inject_context_in_value(value, context_variables)
7
+ inject_context_in_value_custom(value) do |context_variable_key|
8
+ context_variables.get_value(context_variable_key).to_s
9
+ end
10
+ end
11
+
12
+ def self.inject_context_in_value_custom(value)
13
+ return value unless value.is_a?(String)
14
+
15
+ value_with_context_variables_injected = value
16
+ regex = /{{([^}]+)}}/
17
+ encountered_variables = []
18
+
19
+ while (match = regex.match(value_with_context_variables_injected))
20
+ context_variable_key = match[1]
21
+
22
+ unless encountered_variables.include?(context_variable_key)
23
+ value_with_context_variables_injected.gsub!(
24
+ /{{#{context_variable_key}}}/,
25
+ yield(context_variable_key)
26
+ )
27
+ end
28
+
29
+ encountered_variables.push(context_variable_key)
30
+ end
31
+
32
+ value_with_context_variables_injected
33
+ end
34
+
35
+ def self.inject_context_in_filter(filter, context_variables)
36
+ return nil unless filter
37
+
38
+ if filter.is_a?(ConditionTreeBranch)
39
+ return ConditionTreeBranch.new(
40
+ filter.aggregator,
41
+ filter.conditions.map { |condition| inject_context_in_filter(condition, context_variables) }
42
+ )
43
+ end
44
+
45
+ ConditionTreeLeaf.new(
46
+ filter.field,
47
+ filter.operator,
48
+ inject_context_in_value(filter.value, context_variables)
49
+ )
50
+ end
51
+ end
52
+ end
53
+ end
@@ -7,7 +7,7 @@ module ForestAdminAgent
7
7
  class SchemaEmitter
8
8
  LIANA_NAME = "forest-rails"
9
9
 
10
- LIANA_VERSION = "1.0.0-beta.21"
10
+ LIANA_VERSION = "1.0.0-beta.22"
11
11
 
12
12
  def self.get_serialized_schema(datasource)
13
13
  schema_path = Facades::Container.cache(:schema_path)
@@ -1,3 +1,3 @@
1
1
  module ForestAdminAgent
2
- VERSION = "1.0.0-beta.21"
2
+ VERSION = "1.0.0-beta.22"
3
3
  end
@@ -3,6 +3,7 @@ require 'zeitwerk'
3
3
 
4
4
  loader = Zeitwerk::Loader.for_gem
5
5
  loader.inflector.inflect('oauth2' => 'OAuth2')
6
+ loader.inflector.inflect('sse_cache_invalidation' => 'SSECacheInvalidation')
6
7
  loader.setup
7
8
 
8
9
  module ForestAdminAgent
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forest_admin_agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre.beta.21
4
+ version: 1.0.0.pre.beta.22
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthieu
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2023-11-20 00:00:00.000000000 Z
12
+ date: 2023-12-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -53,6 +53,20 @@ dependencies:
53
53
  - - "~>"
54
54
  - !ruby/object:Gem::Version
55
55
  version: '2.7'
56
+ - !ruby/object:Gem::Dependency
57
+ name: filecache
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '1.0'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '1.0'
56
70
  - !ruby/object:Gem::Dependency
57
71
  name: ipaddress
58
72
  requirement: !ruby/object:Gem::Requirement
@@ -96,19 +110,19 @@ dependencies:
96
110
  - !ruby/object:Gem::Version
97
111
  version: '2.7'
98
112
  - !ruby/object:Gem::Dependency
99
- name: lightly
113
+ name: ld-eventsource
100
114
  requirement: !ruby/object:Gem::Requirement
101
115
  requirements:
102
116
  - - "~>"
103
117
  - !ruby/object:Gem::Version
104
- version: 0.4.0
118
+ version: '2.2'
105
119
  type: :runtime
106
120
  prerelease: false
107
121
  version_requirements: !ruby/object:Gem::Requirement
108
122
  requirements:
109
123
  - - "~>"
110
124
  - !ruby/object:Gem::Version
111
- version: 0.4.0
125
+ version: '2.2'
112
126
  - !ruby/object:Gem::Dependency
113
127
  name: mono_logger
114
128
  requirement: !ruby/object:Gem::Requirement
@@ -205,8 +219,11 @@ files:
205
219
  - lib/forest_admin_agent/facades/container.rb
206
220
  - lib/forest_admin_agent/facades/whitelist.rb
207
221
  - lib/forest_admin_agent/http/Exceptions/authentication_open_id_client.rb
222
+ - lib/forest_admin_agent/http/Exceptions/conflict_error.rb
223
+ - lib/forest_admin_agent/http/Exceptions/forbidden_error.rb
208
224
  - lib/forest_admin_agent/http/Exceptions/http_exception.rb
209
225
  - lib/forest_admin_agent/http/Exceptions/not_found_error.rb
226
+ - lib/forest_admin_agent/http/Exceptions/require_approval.rb
210
227
  - lib/forest_admin_agent/http/forest_admin_api_requester.rb
211
228
  - lib/forest_admin_agent/http/router.rb
212
229
  - lib/forest_admin_agent/routes/abstract_authenticated_route.rb
@@ -229,7 +246,12 @@ files:
229
246
  - lib/forest_admin_agent/serializer/forest_serializer_override.rb
230
247
  - lib/forest_admin_agent/services/ip_whitelist.rb
231
248
  - lib/forest_admin_agent/services/logger_service.rb
249
+ - lib/forest_admin_agent/services/permissions.rb
250
+ - lib/forest_admin_agent/services/smart_action_checker.rb
251
+ - lib/forest_admin_agent/services/sse_cache_invalidation.rb
232
252
  - lib/forest_admin_agent/utils/condition_tree_parser.rb
253
+ - lib/forest_admin_agent/utils/context_variables.rb
254
+ - lib/forest_admin_agent/utils/context_variables_injector.rb
233
255
  - lib/forest_admin_agent/utils/error_messages.rb
234
256
  - lib/forest_admin_agent/utils/id.rb
235
257
  - lib/forest_admin_agent/utils/query_string_parser.rb