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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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