From 5880d7e39def16a5f22615a1dea76edc303ebd9f Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 22 Nov 2023 16:02:30 +0100 Subject: [PATCH 01/49] chore: update default setting for permission_expiration --- packages/forest_admin_rails/lib/forest_admin_rails.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/forest_admin_rails/lib/forest_admin_rails.rb b/packages/forest_admin_rails/lib/forest_admin_rails.rb index 9eab9555e..59f5e1ee0 100644 --- a/packages/forest_admin_rails/lib/forest_admin_rails.rb +++ b/packages/forest_admin_rails/lib/forest_admin_rails.rb @@ -18,7 +18,7 @@ module ForestAdminRails setting :forest_server_url, default: 'https://api.forestadmin.com' setting :is_production, default: Rails.env.production? setting :prefix, default: 'forest' - setting :permission_expiration, default: 300 + setting :permission_expiration, default: 900 setting :cache_dir, default: :'tmp/cache/forest_admin' setting :schema_path, default: File.join(Dir.pwd, '.forestadmin-schema.json') setting :project_dir, default: Dir.pwd From 26b33c98dbe494d722a4f2e585456efdfbb77f53 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 22 Nov 2023 16:03:17 +0100 Subject: [PATCH 02/49] chore: add new http exception forbidden --- .../http/Exceptions/forbidden_error.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/forbidden_error.rb diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/forbidden_error.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/forbidden_error.rb new file mode 100644 index 000000000..90d24d675 --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/forbidden_error.rb @@ -0,0 +1,11 @@ +module ForestAdminAgent + module Http + module Exceptions + class ForbiddenError < HttpException + def initialize(message = 'Forbidden') + super 403, 'Forbidden', message + end + end + end + end +end From a3b6d207f4545d3dd20aa8a85772439c1565c3cd Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 22 Nov 2023 16:05:42 +0100 Subject: [PATCH 03/49] chore: add new method handle error on requester --- .rubocop.yml | 1 + .../http/forest_admin_api_requester.rb | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/.rubocop.yml b/.rubocop.yml index f7953c122..311421ca9 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -222,6 +222,7 @@ Layout/LineLength: Max: 120 Exclude: - 'packages/forest_admin_agent/spec/shared/caller.rb' + - 'packages/forest_admin_agent/lib/forest_admin_agent/http/forest_admin_api_requester.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/condition_tree_factory.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/collection.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/filter_factory.rb' diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/forest_admin_api_requester.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/forest_admin_api_requester.rb index 98d07cf4f..d71c9e2de 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/http/forest_admin_api_requester.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/forest_admin_api_requester.rb @@ -3,6 +3,8 @@ module ForestAdminAgent module Http class ForestAdminApiRequester + include ForestAdminDatasourceToolkit::Exceptions + def initialize @headers = { 'Content-Type' => 'application/json', @@ -23,6 +25,32 @@ def get(url, params) def post(url, params) @client.post(url, params.to_json) end + + def self.handle_response_error(error) + raise error if error.is_a?(ForestException) + + if error.message.include?('certificate') + raise ForestException, + 'ForestAdmin server TLS certificate cannot be verified. Please check that your system time is set properly.' + end + + if error.code.zero? || error.code == 502 + raise ForestException, 'Failed to reach ForestAdmin server. Are you online?' + end + + if error.code == 404 + raise ForestException, + '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?' + end + + if error.code == 503 + raise ForestException, + '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.' + end + + raise ForestException, + 'An unexpected error occurred while contacting the ForestAdmin server. Please contact support@forestadmin.com for further investigations.' + end end end end From bdc660b2b7b1ff7dc7d854f63e802b35c563c737 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 22 Nov 2023 16:43:07 +0100 Subject: [PATCH 04/49] fix: requester handling error --- .../forest_admin_agent/http/forest_admin_api_requester.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/forest_admin_api_requester.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/forest_admin_api_requester.rb index d71c9e2de..58bfe08b2 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/http/forest_admin_api_requester.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/forest_admin_api_requester.rb @@ -29,21 +29,21 @@ def post(url, params) def self.handle_response_error(error) raise error if error.is_a?(ForestException) - if error.message.include?('certificate') + if error.response[:message]&.include?('certificate') raise ForestException, 'ForestAdmin server TLS certificate cannot be verified. Please check that your system time is set properly.' end - if error.code.zero? || error.code == 502 + if error.response[:status].zero? || error.response[:status] == 502 raise ForestException, 'Failed to reach ForestAdmin server. Are you online?' end - if error.code == 404 + if error.response[:status] == 404 raise ForestException, '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?' end - if error.code == 503 + if error.response[:status] == 503 raise ForestException, '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.' end From a4ca26aa6e63f28115788233642943d6cecf35c6 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 22 Nov 2023 16:43:28 +0100 Subject: [PATCH 05/49] chore: add remaining test on api requester --- .rubocop.yml | 1 + .../http/forest_admin_api_requester_spec.rb | 62 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/.rubocop.yml b/.rubocop.yml index 311421ca9..0b8546a2d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -223,6 +223,7 @@ Layout/LineLength: Exclude: - 'packages/forest_admin_agent/spec/shared/caller.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/http/forest_admin_api_requester.rb' + - 'packages/forest_admin_agent/spec/lib/forest_admin_agent/http/forest_admin_api_requester_spec.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/condition_tree_factory.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/collection.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/filter_factory.rb' diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/http/forest_admin_api_requester_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/http/forest_admin_api_requester_spec.rb index b3f83020c..82240b9e9 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/http/forest_admin_api_requester_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/http/forest_admin_api_requester_spec.rb @@ -30,6 +30,68 @@ module Http expect(result.body).to eq({}) end end + + context 'when the handle response method is called' do + it 'raises an exception when the error is a ForestException' do + expect do + described_class.handle_response_error(ForestAdminDatasourceToolkit::Exceptions::ForestException.new('test')) + end.to raise_error(ForestAdminDatasourceToolkit::Exceptions::ForestException) + end + + it 'raises an exception when the error message contains certificate' do + expect do + described_class.handle_response_error(Faraday::ConnectionFailed.new('test', { message: 'certificate' })) + end.to raise_error( + ForestAdminDatasourceToolkit::Exceptions::ForestException, + '🌳🌳🌳 ForestAdmin server TLS certificate cannot be verified. Please check that your system time is set properly.' + ) + end + + it 'raises an exception when the error code is 0' do + expect do + described_class.handle_response_error(Faraday::ConnectionFailed.new('test', { status: 0 })) + end.to raise_error( + ForestAdminDatasourceToolkit::Exceptions::ForestException, + '🌳🌳🌳 Failed to reach ForestAdmin server. Are you online?' + ) + end + + it 'raises an exception when the error code is 502' do + expect do + described_class.handle_response_error(Faraday::ConnectionFailed.new('test', { status: 502 })) + end.to raise_error( + ForestAdminDatasourceToolkit::Exceptions::ForestException, + '🌳🌳🌳 Failed to reach ForestAdmin server. Are you online?' + ) + end + + it 'raises an exception when the error code is 404' do + expect do + described_class.handle_response_error(Faraday::ConnectionFailed.new('test', { status: 404 })) + end.to raise_error( + ForestAdminDatasourceToolkit::Exceptions::ForestException, + '🌳🌳🌳 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?' + ) + end + + it 'raises an exception when the error code is 503' do + expect do + described_class.handle_response_error(Faraday::ConnectionFailed.new('test', { status: 503 })) + end.to raise_error( + ForestAdminDatasourceToolkit::Exceptions::ForestException, + '🌳🌳🌳 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.' + ) + end + + it 'raises an exception when the error code is 500' do + expect do + described_class.handle_response_error(Faraday::ConnectionFailed.new('test', { status: 500 })) + end.to raise_error( + ForestAdminDatasourceToolkit::Exceptions::ForestException, + '🌳🌳🌳 An unexpected error occurred while contacting the ForestAdmin server. Please contact support@forestadmin.com for further investigations.' + ) + end + end end end end From 186791fb7f3b2413293f4d98f655119f73e0e418 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 22 Nov 2023 18:11:28 +0100 Subject: [PATCH 06/49] feat(permissions): add new permission service --- .rubocop.yml | 2 + .../services/permissions.rb | 259 ++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb diff --git a/.rubocop.yml b/.rubocop.yml index 0b8546a2d..0658b709c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -201,6 +201,7 @@ Metrics/BlockLength: Metrics/ClassLength: Exclude: - 'packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_field.rb' + - 'packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/collection.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/filter_factory.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/nodes/condition_tree_leaf.rb' @@ -224,6 +225,7 @@ Layout/LineLength: - 'packages/forest_admin_agent/spec/shared/caller.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/http/forest_admin_api_requester.rb' - 'packages/forest_admin_agent/spec/lib/forest_admin_agent/http/forest_admin_api_requester_spec.rb' + - 'packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/condition_tree_factory.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/collection.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/filter_factory.rb' diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb new file mode 100644 index 000000000..7be822681 --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb @@ -0,0 +1,259 @@ +module ForestAdminAgent + module Services + class Permissions + include ForestAdminAgent::Http::Exceptions + include ForestAdminDatasourceToolkit::Exceptions + include ForestAdminDatasourceToolkit::Components::Query::ConditionTree + + attr_reader :caller, :forest_api, :cache + + def initialize(caller) + @caller = caller + @forest_api = ForestAdminAgent::Http::ForestAdminApiRequester.new + @cache = Lightly.new( + life: Facades::Container.config_from_cache[:permission_expiration], + dir: "#{Facades::Container.config_from_cache[:cache_dir]}/permissions" + ) + end + + def invalidate_cache(id_cache) + # TODO: HANDLE CACHE + # ... + # TODO: HANDLE LOGGER + # logger.debug("Invalidating #{id_cache} cache..") + end + + def can?(action, collection, allow_fetch: false) + return true unless permission_system? + + user_data = get_user_data(caller.id) + collections_data = get_collections_permissions_data(force_fetch: allow_fetch) + + is_allowed = collections_data.key?(collection.name) && collections_data[collection.name][action].include?(user_data['roleId']) + + # Refetch + unless is_allowed + collections_data = get_collections_permissions_data(force_fetch: true) + is_allowed = collections_data[collection.name][action].include?(user_data['roleId']) + end + + # still not allowed - throw forbidden message + raise ForbiddenError, "You don't have permission to #{action} this collection." unless is_allowed + + is_allowed + end + + def can_chart?(parameters) + attributes = sanitize_chart_parameters(parameters) + + hash_request = "#{attributes["type"]}:#{array_hash(attributes)}" + is_allowed = get_chart_data(caller.rendering_id).include?(hash_request) + + # Refetch + is_allowed ||= get_chart_data(caller.rendering_id, force_fetch: true).include?(hash_request) + + # still not allowed - throw forbidden message + unless is_allowed + # TODO: HANDLE LOGGER + # logger.debug("User #{caller.id} cannot retrieve chart on rendering #{caller.rendering_id}") + raise ForbiddenError, "You don't have permission to access this collection." + end + + # TODO: HANDLE LOGGER + # logger.debug("User #{caller.id} can retrieve chart on rendering #{caller.rendering_id}") + + is_allowed + end + + def can_smart_action?(request, collection, filter, allow_fetch: true) + return true unless permission_system? + + user_data = get_user_data(caller.id) + collections_data = get_collections_permissions_data(allow_fetch) + action = find_action_from_endpoint(collection.name, request.path_info, request.method) + + raise ForestException, "The collection #{collection.name} does not have this smart action" if action.nil? + + smart_action_approval = SmartActionChecker.new( + request, + collection, + collections_data[collection.name]['actions'][action['name']], + caller, + user_data['roleId'], + filter + ) + + smart_action_approval.can_execute? + # TODO: HANDLE LOGGER + # logger.debug("User #{user_data['roleId']} is #{is_allowed ? '' : 'not'} allowed to perform #{action['name']}") + end + + def get_scope(collection) + permissions = get_scope_and_team_data(caller.rendering_id) + scope = permissions['scopes'][collection.name] + team = permissions['team'] + user = get_user_data(caller.id) + + return nil if scope.nil? + + context_variables = ContextVariables.new(team: team, user: user) + + ContextVariablesInjector.inject_context_in_filter(scope, context_variables) + end + + def get_user_data(user_id) + cache.get('forest.users') do + response = forest_api.fetch('/liana/v4/permissions/users') + users = {} + response.each do |user| + users[user['id'].to_s] = user + end + + # TODO: HANDLE LOGGER + # logger.debug('Refreshing user permissions cache') + + users + end[user_id.to_s] + end + + def get_team(rendering_id) + permissions = get_scope_and_team_data(rendering_id) + + permissions['team'] + end + + private + + def get_collections_permissions_data(force_fetch: false) + invalidate_cache('forest.collections') if force_fetch == true + + cache.get('forest.collections') do + response = forest_api.fetch('/liana/v4/permissions/environment') + collections = {} + response['collections'].each do |name, collection| + collections[name] = + format_collection_crud_permission(collection).merge!(format_collection_action_permission(collection)) + end + + # TODO: HANDLE LOGGER + # logger.debug('Fetching environment permissions') + + collections + end + end + + def get_chart_data(rendering_id, force_fetch: false) + invalidate_cache('forest.stats') if force_fetch == true + + cache.get('forest.stats') do + response = forest_api.fetch("/liana/v4/permissions/renderings/#{rendering_id}") + stat_hash = [] + response['stats'].each do |stat| + stat = stat.select { |_, value| !value.nil? && value != '' } + stat_hash << "#{stat["type"]}:#{array_hash(stat)}" + end + + # TODO: HANDLE LOGGER + # logger.debug("Loading rendering permissions for rendering #{rendering_id}") + + stat_hash + end + end + + def sanitize_chart_parameters(parameters) + parameters = parameters.to_h + parameters.delete('timezone') + parameters.delete('collection') + parameters.delete('contextVariables') + + parameters.select { |_, value| !value.nil? && value != '' } + end + + # protected function arrayHash(array $data): string + # { + # ArrayHelper::ksortRecursive($data); + # + # return sha1(json_encode($data, JSON_THROW_ON_ERROR)); + # } + def array_hash(data) + data.deep_sort.to_s + + Digest::SHA1.hexdigest(data.to_json) + end + + def get_scope_and_team_data(rendering_id) + cache.get('forest.scopes') do + data = {} + response = forest_api.fetch("/liana/v4/permissions/renderings/#{rendering_id}") + + data['scopes'] = decode_scope_permissions(response['collections']) + data['team'] = response['team'] + + data + end + end + + def permission_system? + cache.get('forest.has_permission') do + response = forest_api.fetch('/liana/v4/permissions/environment') + + !response.nil? + end + end + + def find_action_from_endpoint(collection_name, endpoint, http_method) + actions = ForestSchema.get_smart_actions(collection_name) + + return nil if actions.empty? + + actions.find { |action| action['endpoint'] == endpoint && action['httpMethod'] == http_method } + end + + def decode_crud_permissions(collection) + { + 'browse' => collection['collection']['browseEnabled']['roles'], + 'read' => collection['collection']['readEnabled']['roles'], + 'edit' => collection['collection']['editEnabled']['roles'], + 'add' => collection['collection']['addEnabled']['roles'], + 'delete' => collection['collection']['deleteEnabled']['roles'], + 'export' => collection['collection']['exportEnabled']['roles'] + } + end + + def decode_acction_permissions(collection) + actions = {} + actions[:actions] = {} + collection['actions'].each do |id, action| + actions[:actions][id] = { + 'triggerEnabled' => action['triggerEnabled']['roles'], + 'triggerConditions' => action['triggerConditions'], + 'approvalRequired' => action['approvalRequired']['roles'], + 'approvalRequiredConditions' => action['approvalRequiredConditions'], + 'userApprovalEnabled' => action['userApprovalEnabled']['roles'], + 'userApprovalConditions' => action['userApprovalConditions'], + 'selfApprovalEnabled' => action['selfApprovalEnabled']['roles'] + } + end + + actions + end + + def decode_scope_permissions(raw_permissions) + scopes = {} + raw_permissions.each do |collection_name, value| + scopes[collection_name] = ConditionTreeFactory.from_plain_object(value['scope']) unless value['scope'].nil? + end + + scopes + end + + def fetch(url) + response = forest_api.get(url, {}) + + JSON.parse(response.body) + rescue StandardError => e + forest_api.handle_response_error(e) + end + end + end +end From cb8f50d0eb38bc9c2c3eb7283856318cbf68fa87 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 23 Nov 2023 17:10:58 +0100 Subject: [PATCH 07/49] chore: add utils context variable --- .../utils/context_variables.rb | 39 ++++++++++++++++ .../utils/context_variables_spec.rb | 45 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 packages/forest_admin_agent/lib/forest_admin_agent/utils/context_variables.rb create mode 100644 packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_spec.rb diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/utils/context_variables.rb b/packages/forest_admin_agent/lib/forest_admin_agent/utils/context_variables.rb new file mode 100644 index 000000000..e555682ed --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/utils/context_variables.rb @@ -0,0 +1,39 @@ +module ForestAdminAgent + module Utils + class ContextVariables + attr_reader :team, :user, :request_context_variables + + USER_VALUE_PREFIX = 'currentUser.'.freeze + + USER_VALUE_TAG_PREFIX = 'currentUser.tags.'.freeze + + USER_VALUE_TEAM_PREFIX = 'currentUser.team.'.freeze + + def initialize(team, user, request_context_variables = nil) + @team = team + @user = user + @request_context_variables = request_context_variables + end + + def get_value(context_variable_key) + return get_current_user_data(context_variable_key) if context_variable_key.start_with?(USER_VALUE_PREFIX) + + request_context_variables[context_variable_key] + end + + private + + def get_current_user_data(context_variable_key) + if context_variable_key.start_with?(USER_VALUE_TEAM_PREFIX) + return team[context_variable_key[USER_VALUE_TEAM_PREFIX.length..]] + end + + if context_variable_key.start_with?(USER_VALUE_TAG_PREFIX) + return user['tags'][context_variable_key[USER_VALUE_TAG_PREFIX.length..]] + end + + user[context_variable_key[USER_VALUE_PREFIX.length..]] + end + end + end +end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_spec.rb new file mode 100644 index 000000000..da4771e6c --- /dev/null +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +module ForestAdminAgent + module Utils + describe ContextVariables do + let(:user) do + { + 'id' => 1, + 'firstName' => 'John', + 'lastName' => 'Doe', + 'fullName' => 'John Doe', + 'email' => 'John Doe', + 'tags' => { 'foo' => 'bar' }, + 'roleId' => 1, + 'permissionLevel' => 'admin' + } + end + + let(:team) do + { + 'id' => 1, + 'name' => 'Operations' + } + end + + let(:request_context_variables) do + { + 'foo.id' => 100 + } + end + + it 'returns the request context variable key when the key is not present into the user data' do + context_variables = described_class.new(team, user, request_context_variables) + expect(context_variables.get_value('foo.id')).to eq(100) + end + + it 'returns the corresponding value from the key provided of the user data' do + context_variables = described_class.new(team, user, request_context_variables) + expect(context_variables.get_value('currentUser.firstName')).to eq('John') + expect(context_variables.get_value('currentUser.tags.foo')).to eq('bar') + expect(context_variables.get_value('currentUser.team.id')).to eq(1) + end + end + end +end From b27bdd9a36aca01a8d88d2484809c5cd6f471a1a Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 24 Nov 2023 17:38:22 +0100 Subject: [PATCH 08/49] chore: rewrite context variable injector --- .../utils/context_variables_injector.rb | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 packages/forest_admin_agent/lib/forest_admin_agent/utils/context_variables_injector.rb diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/utils/context_variables_injector.rb b/packages/forest_admin_agent/lib/forest_admin_agent/utils/context_variables_injector.rb new file mode 100644 index 000000000..4e2c0673c --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/utils/context_variables_injector.rb @@ -0,0 +1,53 @@ +module ForestAdminAgent + module Utils + class ContextVariablesInjector + include ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes + + def self.inject_context_in_value(value, context_variables) + inject_context_in_value_custom(value) do |context_variable_key| + context_variables.get_value(context_variable_key).to_s + end + end + + def self.inject_context_in_value_custom(value) + return value unless value.is_a?(String) + + value_with_context_variables_injected = value + regex = /{{([^}]+)}}/ + encountered_variables = [] + + while (match = regex.match(value_with_context_variables_injected)) + context_variable_key = match[1] + + unless encountered_variables.include?(context_variable_key) + value_with_context_variables_injected.gsub!( + /{{#{context_variable_key}}}/, + yield(context_variable_key) + ) + end + + encountered_variables.push(context_variable_key) + end + + value_with_context_variables_injected + end + + def self.inject_context_in_filter(filter, context_variables) + return nil unless filter + + if filter.is_a?(ConditionTreeBranch) + return ConditionTreeBranch.new( + filter.aggregator, + filter.conditions.map { |condition| inject_context_in_filter(condition, context_variables) } + ) + end + + ConditionTreeLeaf.new( + filter.field, + filter.operator, + inject_context_in_value(filter.value, context_variables) + ) + end + end + end +end From 4aae1bddefdc34d1300af2004de5f44fa9f1cd45 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 28 Nov 2023 11:07:11 +0100 Subject: [PATCH 09/49] chore: add test on context variable injector --- .rubocop.yml | 1 + .../utils/context_variables_injector_spec.rb | 109 ++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_injector_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 0658b709c..51a719404 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -226,6 +226,7 @@ Layout/LineLength: - 'packages/forest_admin_agent/lib/forest_admin_agent/http/forest_admin_api_requester.rb' - 'packages/forest_admin_agent/spec/lib/forest_admin_agent/http/forest_admin_api_requester_spec.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb' + - 'packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_injector_spec.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/condition_tree_factory.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/collection.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/filter_factory.rb' diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_injector_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_injector_spec.rb new file mode 100644 index 000000000..22942274f --- /dev/null +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_injector_spec.rb @@ -0,0 +1,109 @@ +require 'spec_helper' + +module ForestAdminAgent + module Utils + describe ContextVariablesInjector do + let(:team) { { 'id' => 100, 'name' => 'Ninja' } } + + let(:user) do + { + 'id' => 1, + 'firstName' => 'John', + 'lastName' => 'Doe', + 'fullName' => 'John Doe', + 'email' => 'John Doe', + 'tags' => { 'planet' => 'Death Star' }, + 'roleId' => 1, + 'permissionLevel' => 'admin' + } + end + + let(:context_variables) do + ForestAdminAgent::Utils::ContextVariables.new( + team, + user, + { + 'siths.selectedRecord.rank' => 3, + 'siths.selectedRecord.power' => 'electrocute' + } + ) + end + + context 'when inject_context_in_filter is called' do + it 'returns it as it is with a number' do + result = described_class.inject_context_in_value_custom(8) { {} } + + expect(result).to eq(8) + end + + it 'returns it as it is with a array' do + value = ['test', 'me'] + result = described_class.inject_context_in_value_custom(value) { {} } + + expect(result).to eq(value) + end + + it 'replaces all variables with a string' do + replace_function = ->(key) { key.split('.').pop.upcase } + result = described_class.inject_context_in_value_custom( + 'It should be {{siths.selectedRecord.power}} of rank {{siths.selectedRecord.rank}}. But {{siths.selectedRecord.power}} can be duplicated.' + ) do |key| + replace_function.call(key) + end + + expect(result).to eq('It should be POWER of rank RANK. But POWER can be duplicated.') + end + end + + context('when inject_context_in_value is called') do + it 'returns it as it is with a number' do + result = described_class.inject_context_in_value(8, context_variables) + + expect(result).to eq(8) + end + + it 'returns it as it is with a array' do + value = ['test', 'me'] + result = described_class.inject_context_in_value(value, context_variables) + + expect(result).to eq(value) + end + + it 'replaces all variables with a string' do + first_value_part = 'It should be {{siths.selectedRecord.power}} of rank {{siths.selectedRecord.rank}}.' + second_value_part = 'But {{siths.selectedRecord.power}} can be duplicated.' + result = described_class.inject_context_in_value( + "#{first_value_part} #{second_value_part}", + context_variables + ) + + expect(result).to eq('It should be electrocute of rank 3. But electrocute can be duplicated.') + end + + it 'replaces all currentUser variables' do + [ + { key: 'email', expected_value: user['email'] }, + { key: 'firstName', expected_value: user['firstName'] }, + { key: 'lastName', expected_value: user['lastName'] }, + { key: 'fullName', expected_value: user['fullName'] }, + { key: 'id', expected_value: user['id'] }, + { key: 'permissionLevel', expected_value: user['permissionLevel'] }, + { key: 'roleId', expected_value: user['roleId'] }, + { key: 'tags.planet', expected_value: user['tags']['planet'] }, + { key: 'team.id', expected_value: team['id'] }, + { key: 'team.name', expected_value: team['name'] } + ].each do |value| + key = value[:key] + expected_value = value[:expected_value] + expect( + described_class.inject_context_in_value( + "{{currentUser.#{key}}}", + context_variables + ) + ).to eq(expected_value.to_s) + end + end + end + end + end +end From 9e91124bde49a118160c9be051a0ffd4c85fb542 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 28 Nov 2023 12:08:07 +0100 Subject: [PATCH 10/49] feat(permissions): add new http exceptions --- .../http/Exceptions/conflict_error.rb | 14 ++++++++++++++ .../http/Exceptions/forbidden_error.rb | 5 ++++- .../http/Exceptions/require_approval.rb | 15 +++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/conflict_error.rb create mode 100644 packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/require_approval.rb diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/conflict_error.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/conflict_error.rb new file mode 100644 index 000000000..daba11493 --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/conflict_error.rb @@ -0,0 +1,14 @@ +module ForestAdminAgent + module Http + module Exceptions + class RequireApproval < HttpException + attr_reader :name + + def initialize(message, name = 'ConflictError') + @name = name + super 429, 'Conflict', message + end + end + end + end +end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/forbidden_error.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/forbidden_error.rb index 90d24d675..d05ea1f0f 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/forbidden_error.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/forbidden_error.rb @@ -2,7 +2,10 @@ module ForestAdminAgent module Http module Exceptions class ForbiddenError < HttpException - def initialize(message = 'Forbidden') + attr_reader :name + + def initialize(message, name = 'ForbiddenError') + @name = name super 403, 'Forbidden', message end end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/require_approval.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/require_approval.rb new file mode 100644 index 000000000..ebb4d43eb --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/require_approval.rb @@ -0,0 +1,15 @@ +module ForestAdminAgent + module Http + module Exceptions + class RequireApproval < HttpException + attr_reader :name, :data + + def initialize(message, name = 'RequireApproval', data = []) + @name = name + @data = data + super 403, 'Forbidden', message + end + end + end + end +end From a643af45482e386f96481f5973843ec789605cce Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 28 Nov 2023 14:38:47 +0100 Subject: [PATCH 11/49] feat(permissions): add smart_action_checker class --- .../services/smart_action_checker.rb | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb b/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb new file mode 100644 index 000000000..b472cbce4 --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb @@ -0,0 +1,86 @@ +module ForestAdminAgent + module Services + class SmartActionChecker + include ForestAdminAgent::Http::Exceptions + include ForestAdminAgent::Utils + include ForestAdminDatasourceToolkit::Utils + include ForestAdminDatasourceToolkit::Components::Query + + TRIGGER_FORBIDDEN_ERROR = 'CustomActionTriggerForbiddenError'.freeze + + INVALID_ACTION_CONDITION_ERROR = 'InvalidActionConditionError'.freeze + + def initialize(parameters, collection, smart_action, user) + @parameters = parameters + @attributes = @parameters[:data][:attributes] + @collection = collection + @smart_action = smart_action + @user = user + end + + def can_execute? + if @attributes[:signed_approval_request].present? && + @smart_action['userApprovalEnabled'].include?(@user['roleId']) + can_approve? + else + can_trigger? + end + end + + private + + def can_approve? + @parameters = RequestPermission.decodeSignedApprovalRequest(@parameters) + if (@smart_action['userApprovalConditions'].empty? || match_conditions('userApprovalConditions')) && + (@attributes[:requester_id] != @user['id'] || @smart_action['selfApprovalEnabled'].include?(@user['roleId'])) + + return true + end + + raise ForbiddenError.new('You don\'t have the permission to trigger this action.', TRIGGER_FORBIDDEN_ERROR) + end + + def can_trigger? + if @smart_action['triggerEnabled'].include?(@user['roleId']) && + @smart_action['approvalRequired'].exclude?(@user['roleId']) + return true if @smart_action['triggerConditions'].empty? || match_conditions('triggerConditions') + elsif @smart_action['approvalRequired'].include?(@user['roleId']) + if @smart_action['approvalRequiredConditions'].empty? || match_conditions('approvalRequiredConditions') + raise RequireApproval, @smart_action['userApprovalEnabled'] + end + return true if @smart_action['triggerConditions'].empty? || match_conditions('triggerConditions') + end + + raise ForbiddenError.new('You don\'t have the permission to trigger this action.', TRIGGER_FORBIDDEN_ERROR) + end + + def match_conditions(condition_name) + pk = Schema.primary_keys(@collection)[0] + condition_filter = if attributes[:all_records] + Nodes::ConditionTreeLeaf.new(pk, 'NOT_EQUAL', @attributes[:all_records_ids_excluded]) + else + Nodes::ConditionTreeLeaf.new(pk, 'IN', @attributes[:ids]) + end + + condition = @smart_action[condition_name][0]['filter'] + conditional_filter = @filter.override( + condition_tree: ConditionTree::ConditionTreeFactory.intersect( + [ + ConditionTreeParser.from_plain_object(@collection, condition), + @filter.get_condition_tree, + condition_filter + ] + ) + ) + rows = @collection.aggregate(@caller, conditional_filter, Aggregation.new(operation: 'Count')) + + (rows[0]['value'] || 0) == attributes[:ids].count + rescue StandardError + raise ConflictError.new( + 'The conditions to trigger this action cannot be verified. Please contact an administrator.', + INVALID_ACTION_CONDITION_ERROR + ) + end + end + end +end From 5ac18a0c7cbf3d479b5bbc1e4e30846c8e7ede1b Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 28 Nov 2023 17:00:35 +0100 Subject: [PATCH 12/49] refactor: smart_action_checker --- .rubocop.yml | 5 +-- .../services/smart_action_checker.rb | 35 ++++++++++--------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 51a719404..e6395d7f7 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -161,13 +161,14 @@ Naming/PredicateName: Metrics/ParameterLists: Exclude: + - 'packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb' + - 'packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/list_related_spec.rb' + - 'packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/count_related_spec.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/many_to_many_schema.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/column_schema.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/caller.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/filter.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/collection.rb' - - 'packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/list_related_spec.rb' - - 'packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/count_related_spec.rb' Metrics/ModuleLength: CountAsOne: [ 'array', 'hash', 'method_call' ] diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb b/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb index b472cbce4..ab5af0d96 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb @@ -10,16 +10,18 @@ class SmartActionChecker INVALID_ACTION_CONDITION_ERROR = 'InvalidActionConditionError'.freeze - def initialize(parameters, collection, smart_action, user) + def initialize(parameters, collection, smart_action, caller, role_id, filter) @parameters = parameters - @attributes = @parameters[:data][:attributes] @collection = collection @smart_action = smart_action - @user = user + @caller = caller + @role_id = role_id + @filter = filter + @attributes = parameters[:data][:attributes] end def can_execute? - if @attributes[:signed_approval_request].present? && + if @attributes[:signed_approval_request] && @smart_action['userApprovalEnabled'].include?(@user['roleId']) can_approve? else @@ -30,10 +32,9 @@ def can_execute? private def can_approve? - @parameters = RequestPermission.decodeSignedApprovalRequest(@parameters) - if (@smart_action['userApprovalConditions'].empty? || match_conditions('userApprovalConditions')) && - (@attributes[:requester_id] != @user['id'] || @smart_action['selfApprovalEnabled'].include?(@user['roleId'])) - + if (@smart_action.include?(@role_id) && + (@smart_action['userApprovalConditions'].empty? || match_conditions('userApprovalConditions'))) && + (@attributes[:requester_id] != @caller.id || @smart_action['selfApprovalEnabled'].include?(@role_id)) return true end @@ -41,17 +42,19 @@ def can_approve? end def can_trigger? - if @smart_action['triggerEnabled'].include?(@user['roleId']) && - @smart_action['approvalRequired'].exclude?(@user['roleId']) - return true if @smart_action['triggerConditions'].empty? || match_conditions('triggerConditions') - elsif @smart_action['approvalRequired'].include?(@user['roleId']) + if @smart_action.include?(@role_id) && !@smart_action['approvalRequired'].include?(@role_id) + true if @smart_action['triggerConditions'].empty? || match_conditions('triggerConditions') + elsif @smart_action['approvalRequired'].include?(@role_id) && @smart_action['triggerEnabled'].include?(@role_id) if @smart_action['approvalRequiredConditions'].empty? || match_conditions('approvalRequiredConditions') - raise RequireApproval, @smart_action['userApprovalEnabled'] + raise RequireApprovalError.new( + 'This action requires to be approved.', + 'CustomActionRequiresApprovalError', + @smart_action['userApprovalEnabled'] + ) + elsif @smart_action['triggerConditions'].empty? || match_conditions('triggerConditions') + true end - return true if @smart_action['triggerConditions'].empty? || match_conditions('triggerConditions') end - - raise ForbiddenError.new('You don\'t have the permission to trigger this action.', TRIGGER_FORBIDDEN_ERROR) end def match_conditions(condition_name) From f89cefa0c89bb0d728709c0a5b513ecbe40a693e Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 29 Nov 2023 15:06:03 +0100 Subject: [PATCH 13/49] fix(permissions): smart_action_checker --- .../services/smart_action_checker.rb | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb b/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb index ab5af0d96..398ede4c5 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb @@ -5,9 +5,12 @@ class SmartActionChecker include ForestAdminAgent::Utils include ForestAdminDatasourceToolkit::Utils include ForestAdminDatasourceToolkit::Components::Query + include ForestAdminDatasourceToolkit::Components::Query::ConditionTree TRIGGER_FORBIDDEN_ERROR = 'CustomActionTriggerForbiddenError'.freeze + REQUIRE_APPROVAL_ERROR = 'CustomActionRequiresApprovalError'.freeze + INVALID_ACTION_CONDITION_ERROR = 'InvalidActionConditionError'.freeze def initialize(parameters, collection, smart_action, caller, role_id, filter) @@ -21,19 +24,18 @@ def initialize(parameters, collection, smart_action, caller, role_id, filter) end def can_execute? - if @attributes[:signed_approval_request] && - @smart_action['userApprovalEnabled'].include?(@user['roleId']) - can_approve? - else + if @attributes[:signed_approval_request].nil? can_trigger? + else + can_approve? end end private def can_approve? - if (@smart_action.include?(@role_id) && - (@smart_action['userApprovalConditions'].empty? || match_conditions('userApprovalConditions'))) && + if @smart_action['userApprovalEnabled'].include?(@role_id) && + (@smart_action['userApprovalConditions'].empty? || match_conditions('userApprovalConditions')) && (@attributes[:requester_id] != @caller.id || @smart_action['selfApprovalEnabled'].include?(@role_id)) return true end @@ -42,24 +44,27 @@ def can_approve? end def can_trigger? - if @smart_action.include?(@role_id) && !@smart_action['approvalRequired'].include?(@role_id) - true if @smart_action['triggerConditions'].empty? || match_conditions('triggerConditions') + if @smart_action['triggerEnabled'].include?(@role_id) && !@smart_action['approvalRequired'].include?(@role_id) + return true if @smart_action['triggerConditions'].empty? || match_conditions('triggerConditions') elsif @smart_action['approvalRequired'].include?(@role_id) && @smart_action['triggerEnabled'].include?(@role_id) if @smart_action['approvalRequiredConditions'].empty? || match_conditions('approvalRequiredConditions') - raise RequireApprovalError.new( + raise RequireApproval.new( 'This action requires to be approved.', - 'CustomActionRequiresApprovalError', + REQUIRE_APPROVAL_ERROR, @smart_action['userApprovalEnabled'] ) elsif @smart_action['triggerConditions'].empty? || match_conditions('triggerConditions') - true + return true end end + + raise ForbiddenError.new('You don\'t have the permission to trigger this action.', TRIGGER_FORBIDDEN_ERROR) end def match_conditions(condition_name) pk = Schema.primary_keys(@collection)[0] - condition_filter = if attributes[:all_records] + condition_filter = if @attributes[:all_records] + puts 1 Nodes::ConditionTreeLeaf.new(pk, 'NOT_EQUAL', @attributes[:all_records_ids_excluded]) else Nodes::ConditionTreeLeaf.new(pk, 'IN', @attributes[:ids]) @@ -67,17 +72,17 @@ def match_conditions(condition_name) condition = @smart_action[condition_name][0]['filter'] conditional_filter = @filter.override( - condition_tree: ConditionTree::ConditionTreeFactory.intersect( + condition_tree: ConditionTreeFactory.intersect( [ ConditionTreeParser.from_plain_object(@collection, condition), - @filter.get_condition_tree, + @filter.condition_tree, condition_filter ] ) ) rows = @collection.aggregate(@caller, conditional_filter, Aggregation.new(operation: 'Count')) - (rows[0]['value'] || 0) == attributes[:ids].count + (rows[0]['value'] || 0) == @attributes[:ids].count rescue StandardError raise ConflictError.new( 'The conditions to trigger this action cannot be verified. Please contact an administrator.', From 3c3a5af13ac3a076e936ec12a07861b40082d499 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 29 Nov 2023 15:06:27 +0100 Subject: [PATCH 14/49] fix(exception): typo on conflict error class --- .../lib/forest_admin_agent/http/Exceptions/conflict_error.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/conflict_error.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/conflict_error.rb index daba11493..dbdb59f02 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/conflict_error.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/conflict_error.rb @@ -1,7 +1,7 @@ module ForestAdminAgent module Http module Exceptions - class RequireApproval < HttpException + class ConflictError < HttpException attr_reader :name def initialize(message, name = 'ConflictError') From f742585bcac48fde286355c31fb894015010cd8b Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 29 Nov 2023 15:41:20 +0100 Subject: [PATCH 15/49] chore: add getter on smart_action_checker --- .../services/smart_action_checker.rb | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb b/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb index 398ede4c5..ccf0416cc 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb @@ -7,6 +7,8 @@ class SmartActionChecker include ForestAdminDatasourceToolkit::Components::Query include ForestAdminDatasourceToolkit::Components::Query::ConditionTree + attr_reader :parameters, :collection, :smart_action, :caller, :role_id, :filter, :attributes + TRIGGER_FORBIDDEN_ERROR = 'CustomActionTriggerForbiddenError'.freeze REQUIRE_APPROVAL_ERROR = 'CustomActionRequiresApprovalError'.freeze @@ -24,7 +26,7 @@ def initialize(parameters, collection, smart_action, caller, role_id, filter) end def can_execute? - if @attributes[:signed_approval_request].nil? + if attributes[:signed_approval_request].nil? can_trigger? else can_approve? @@ -34,9 +36,9 @@ def can_execute? private def can_approve? - if @smart_action['userApprovalEnabled'].include?(@role_id) && - (@smart_action['userApprovalConditions'].empty? || match_conditions('userApprovalConditions')) && - (@attributes[:requester_id] != @caller.id || @smart_action['selfApprovalEnabled'].include?(@role_id)) + if smart_action['userApprovalEnabled'].include?(role_id) && + (smart_action['userApprovalConditions'].empty? || match_conditions('userApprovalConditions')) && + (attributes[:requester_id] != caller.id || smart_action['selfApprovalEnabled'].include?(role_id)) return true end @@ -44,16 +46,16 @@ def can_approve? end def can_trigger? - if @smart_action['triggerEnabled'].include?(@role_id) && !@smart_action['approvalRequired'].include?(@role_id) - return true if @smart_action['triggerConditions'].empty? || match_conditions('triggerConditions') - elsif @smart_action['approvalRequired'].include?(@role_id) && @smart_action['triggerEnabled'].include?(@role_id) - if @smart_action['approvalRequiredConditions'].empty? || match_conditions('approvalRequiredConditions') + if smart_action['triggerEnabled'].include?(role_id) && !smart_action['approvalRequired'].include?(role_id) + return true if smart_action['triggerConditions'].empty? || match_conditions('triggerConditions') + elsif smart_action['approvalRequired'].include?(role_id) && smart_action['triggerEnabled'].include?(role_id) + if smart_action['approvalRequiredConditions'].empty? || match_conditions('approvalRequiredConditions') raise RequireApproval.new( 'This action requires to be approved.', REQUIRE_APPROVAL_ERROR, - @smart_action['userApprovalEnabled'] + smart_action['userApprovalEnabled'] ) - elsif @smart_action['triggerConditions'].empty? || match_conditions('triggerConditions') + elsif smart_action['triggerConditions'].empty? || match_conditions('triggerConditions') return true end end @@ -62,27 +64,27 @@ def can_trigger? end def match_conditions(condition_name) - pk = Schema.primary_keys(@collection)[0] - condition_filter = if @attributes[:all_records] + pk = Schema.primary_keys(collection)[0] + condition_filter = if attributes[:all_records] puts 1 - Nodes::ConditionTreeLeaf.new(pk, 'NOT_EQUAL', @attributes[:all_records_ids_excluded]) + Nodes::ConditionTreeLeaf.new(pk, 'NOT_EQUAL', attributes[:all_records_ids_excluded]) else - Nodes::ConditionTreeLeaf.new(pk, 'IN', @attributes[:ids]) + Nodes::ConditionTreeLeaf.new(pk, 'IN', attributes[:ids]) end - condition = @smart_action[condition_name][0]['filter'] - conditional_filter = @filter.override( + condition = smart_action[condition_name][0]['filter'] + conditional_filter = filter.override( condition_tree: ConditionTreeFactory.intersect( [ - ConditionTreeParser.from_plain_object(@collection, condition), - @filter.condition_tree, + ConditionTreeParser.from_plain_object(collection, condition), + filter.condition_tree, condition_filter ] ) ) - rows = @collection.aggregate(@caller, conditional_filter, Aggregation.new(operation: 'Count')) + rows = collection.aggregate(caller, conditional_filter, Aggregation.new(operation: 'Count')) - (rows[0]['value'] || 0) == @attributes[:ids].count + (rows[0]['value'] || 0) == attributes[:ids].count rescue StandardError raise ConflictError.new( 'The conditions to trigger this action cannot be verified. Please contact an administrator.', From 77da7d2bb2933cc357d186846db9667b3fb843c9 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 29 Nov 2023 15:42:44 +0100 Subject: [PATCH 16/49] fix(typo): smart_action --- .../lib/forest_admin_agent/services/smart_action_checker.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb b/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb index ccf0416cc..95f03e3c8 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb @@ -66,7 +66,6 @@ def can_trigger? def match_conditions(condition_name) pk = Schema.primary_keys(collection)[0] condition_filter = if attributes[:all_records] - puts 1 Nodes::ConditionTreeLeaf.new(pk, 'NOT_EQUAL', attributes[:all_records_ids_excluded]) else Nodes::ConditionTreeLeaf.new(pk, 'IN', attributes[:ids]) From d7a2222f892e980cdfea68632edcc611c8fd0453 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 29 Nov 2023 16:15:30 +0100 Subject: [PATCH 17/49] chore: add tests on smart_action_checker --- .rubocop.yml | 1 + .../services/smart_action_checker_spec.rb | 564 ++++++++++++++++++ 2 files changed, 565 insertions(+) create mode 100644 packages/forest_admin_agent/spec/lib/forest_admin_agent/services/smart_action_checker_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index e6395d7f7..03994653b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -228,6 +228,7 @@ Layout/LineLength: - 'packages/forest_admin_agent/spec/lib/forest_admin_agent/http/forest_admin_api_requester_spec.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb' - 'packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_injector_spec.rb' + - 'packages/forest_admin_agent/spec/lib/forest_admin_agent/services/smart_action_checker_spec.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/condition_tree_factory.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/collection.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/filter_factory.rb' diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/smart_action_checker_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/smart_action_checker_spec.rb new file mode 100644 index 000000000..27cbb4872 --- /dev/null +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/smart_action_checker_spec.rb @@ -0,0 +1,564 @@ +require 'spec_helper' +require 'shared/caller' + +module ForestAdminAgent + module Services + include ForestAdminAgent::Http::Exceptions + include ForestAdminAgent::Utils + include ForestAdminDatasourceToolkit + include ForestAdminDatasourceToolkit::Schema + include ForestAdminDatasourceToolkit::Components::Query + + describe SmartActionChecker do + include_context 'with caller' + + let :args do + { + headers: { + 'HTTP_AUTHORIZATION' => bearer + }, + params: { + 'timezone' => 'Europe/Paris' + } + } + end + + let :parameters do + { + data: { + attributes: { + values: [], + ids: [1], + collection_name: 'Booking', + parent_collection_name: nil, + parent_collection_id: nil, + parent_association_name: nil, + all_records: false, + all_records_subset_query: { + 'fields[Book]' => 'id,title', + 'page[number]' => 1, + 'page[size]' => 15, + 'sort' => '-id', + 'timezone' => 'Europe/Paris' + }, + all_records_ids_excluded: [], + smart_action_id: 'Booking-Mark@@@as@@@live', + signed_approval_request: nil + }, + type: 'custom-action-requests' + } + } + end + + let :smart_action do + { + 'triggerEnabled' => [], + 'triggerConditions' => [], + 'approvalRequired' => [], + 'approvalRequiredConditions' => [], + 'userApprovalEnabled' => [], + 'userApprovalConditions' => [], + 'selfApprovalEnabled' => [] + } + end + + before do + @datasource = Datasource.new + collection_book = instance_double( + Collection, + name: 'Book', + fields: { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'title' => ColumnSchema.new(column_type: 'String') + } + ) + + allow(ForestAdminAgent::Builder::AgentFactory.instance).to receive(:send_schema).and_return(nil) + @datasource.add_collection(collection_book) + ForestAdminAgent::Builder::AgentFactory.instance.add_datasource(@datasource) + ForestAdminAgent::Builder::AgentFactory.instance.build + end + + it 'returns true when the user can trigger the action' do + smart_action['triggerEnabled'] = [1] + + smart_action_checker = described_class.new(parameters, @datasource.collection('Book'), smart_action, + QueryStringParser.parse_caller(args), 1, Filter.new) + expect(smart_action_checker).to be_can_execute + end + + it 'returns true when the user can trigger the action with trigger conditions' do + smart_action['triggerEnabled'] = [1] + smart_action['triggerConditions'] = [ + { + 'filter' => { + 'aggregator' => 'and', + 'conditions' => [ + { + 'field' => 'title', + 'value' => nil, + 'source' => 'data', + 'operator' => 'present' + } + ] + }, + 'roleId' => 1 + } + ] + + collection = @datasource.collection('Book') + allow(collection).to receive(:aggregate).and_return([{ 'value' => 1, 'group' => [] }]) + + smart_action_checker = described_class.new(parameters, collection, smart_action, + QueryStringParser.parse_caller(args), 1, Filter.new) + expect(smart_action_checker).to be_can_execute + end + + it 'returns true when the user can trigger the action with trigger conditions with all_records_ids_excluded not empty' do + smart_action['triggerEnabled'] = [1] + smart_action['triggerConditions'] = [ + { + 'filter' => { + 'aggregator' => 'and', + 'conditions' => [ + { + 'field' => 'title', + 'value' => nil, + 'source' => 'data', + 'operator' => 'present' + } + ] + }, + 'roleId' => 1 + } + ] + + parameters[:data][:attributes][:all_records_ids_excluded] = [1] + + collection = @datasource.collection('Book') + allow(collection).to receive(:aggregate).and_return([{ 'value' => 1, 'group' => [] }]) + + smart_action_checker = described_class.new(parameters, collection, smart_action, + QueryStringParser.parse_caller(args), 1, Filter.new) + expect(smart_action_checker).to be_can_execute + end + + it 'throws when the user try to trigger the action with approvalRequired and without approvalRequiredConditions' do + smart_action['triggerEnabled'] = [1] + smart_action['approvalRequired'] = [1] + smart_action['approvalRequiredConditions'] = [] + + smart_action_checker = described_class.new(parameters, @datasource.collection('Book'), smart_action, + QueryStringParser.parse_caller(args), 1, Filter.new) + expect do + smart_action_checker.can_execute? + end.to raise_error(RequireApproval, 'This action requires to be approved.') + end + + it 'throws when the user try to trigger the action with approvalRequired and match approvalRequiredConditions' do + smart_action['triggerEnabled'] = [1] + smart_action['approvalRequired'] = [1] + smart_action['approvalRequiredConditions'] = [ + { + 'filter' => { + 'aggregator' => 'and', + 'conditions' => [ + { + 'field' => 'id', + 'value' => 1, + 'source' => 'data', + 'operator' => 'equal' + } + ] + }, + 'roleId' => 1 + } + ] + + collection = @datasource.collection('Book') + allow(collection).to receive(:aggregate).and_return([{ 'value' => 1, 'group' => [] }]) + + smart_action_checker = described_class.new(parameters, collection, smart_action, + QueryStringParser.parse_caller(args), 1, Filter.new) + expect do + smart_action_checker.can_execute? + end.to raise_error(RequireApproval, 'This action requires to be approved.') + end + + it 'returns true when the user try to trigger the action with approvalRequired and triggerConditions and correct role into approvalRequired' do + smart_action['triggerEnabled'] = [1] + smart_action['approvalRequired'] = [1] + smart_action['approvalRequiredConditions'] = [ + { + 'filter' => { + 'aggregator' => 'and', + 'conditions' => [ + { + 'field' => 'id', + 'value' => 1, + 'source' => 'data', + 'operator' => 'equal' + } + ] + }, + 'roleId' => 1 + } + ] + + collection = @datasource.collection('Book') + allow(collection).to receive(:aggregate).and_return([{ 'value' => 0, 'group' => [] }]) + + smart_action_checker = described_class.new(parameters, collection, smart_action, + QueryStringParser.parse_caller(args), 1, Filter.new) + expect(smart_action_checker).to be_can_execute + end + + it 'returns true when the user try to trigger the action with approvalRequired with triggerConditions and correct role into approvalRequired' do + smart_action['triggerEnabled'] = [1] + smart_action['triggerConditions'] = [ + { + 'filter' => { + 'aggregator' => 'and', + 'conditions' => [ + { + 'field' => 'title', + 'value' => nil, + 'source' => 'data', + 'operator' => 'present' + } + ] + }, + 'roleId' => 1 + } + ] + smart_action['approvalRequired'] = [1] + smart_action['approvalRequiredConditions'] = [ + { + 'filter' => { + 'aggregator' => 'and', + 'conditions' => [ + { + 'field' => 'id', + 'value' => 1, + 'source' => 'data', + 'operator' => 'equal' + } + ] + }, + 'roleId' => 1 + } + ] + + collection = @datasource.collection('Book') + allow(collection).to receive(:aggregate).and_return( + [{ 'value' => 0, 'group' => [] }], + [{ 'value' => 1, 'group' => [] }] + ) + + smart_action_checker = described_class.new(parameters, collection, smart_action, + QueryStringParser.parse_caller(args), 1, Filter.new) + expect(smart_action_checker).to be_can_execute + end + + it 'throws when the user roleId is not into triggerEnabled & approvalRequired' do + smart_action['triggerEnabled'] = [1000] + smart_action['triggerConditions'] = [] + smart_action['approvalRequired'] = [1000] + smart_action['approvalRequiredConditions'] = [] + + smart_action_checker = described_class.new(parameters, @datasource.collection('Book'), smart_action, + QueryStringParser.parse_caller(args), 1, Filter.new) + expect do + smart_action_checker.can_execute? + end.to raise_error(ForbiddenError, + "You don't have the permission to trigger this action.") + end + + it "throws when smart action doesn't match with triggerConditions & approvalRequiredConditions" do + smart_action['triggerEnabled'] = [1] + smart_action['triggerConditions'] = [ + { + 'filter' => { + 'aggregator' => 'and', + 'conditions' => [ + { + 'field' => 'title', + 'value' => nil, + 'source' => 'data', + 'operator' => 'present' + } + ] + }, + 'roleId' => 1 + } + ] + smart_action['approvalRequired'] = [1] + smart_action['approvalRequiredConditions'] = [ + { + 'filter' => { + 'aggregator' => 'and', + 'conditions' => [ + { + 'field' => 'id', + 'value' => 1, + 'source' => 'data', + 'operator' => 'equal' + } + ] + }, + 'roleId' => 1 + } + ] + + collection = @datasource.collection('Book') + allow(collection).to receive(:aggregate).and_return( + [{ 'value' => 0, 'group' => [] }], + [{ 'value' => 0, 'group' => [] }] + ) + + smart_action_checker = described_class.new(parameters, collection, smart_action, + QueryStringParser.parse_caller(args), 1, Filter.new) + expect do + smart_action_checker.can_execute? + end.to raise_error(ForbiddenError, + "You don't have the permission to trigger this action.") + end + + it 'returns true when the user can approve and there is no userApprovalConditions and requesterId is not the callerId' do + parameters[:data][:attributes][:requester_id] = 20 + parameters[:data][:attributes][:signed_approval_request] = 'AAABBBCCC' + smart_action['userApprovalEnabled'] = [1] + + smart_action_checker = described_class.new(parameters, @datasource.collection('Book'), smart_action, + QueryStringParser.parse_caller(args), 1, Filter.new) + expect(smart_action_checker).to be_can_execute + end + + it 'returns true when the user can approve and there is no userApprovalConditions and user roleId is present into selfApprovalEnabled' do + parameters[:data][:attributes][:requester_id] = 1 + parameters[:data][:attributes][:signed_approval_request] = 'AAABBBCCC' + smart_action['userApprovalEnabled'] = [1] + smart_action['selfApprovalEnabled'] = [1] + + smart_action_checker = described_class.new(parameters, @datasource.collection('Book'), smart_action, + QueryStringParser.parse_caller(args), 1, Filter.new) + expect(smart_action_checker).to be_can_execute + end + + it 'returns true when the user can approve and the condition match with userApprovalConditions and requesterId is the callerId' do + parameters[:data][:attributes][:requester_id] = 20 + parameters[:data][:attributes][:signed_approval_request] = 'AAABBBCCC' + smart_action['userApprovalEnabled'] = [1] + smart_action['userApprovalConditions'] = [ + { + 'filter' => { + 'aggregator' => 'and', + 'conditions' => [ + { + 'field' => 'id', + 'value' => 1, + 'source' => 'data', + 'operator' => 'equal' + } + ] + }, + 'roleId' => 1 + } + ] + + collection = @datasource.collection('Book') + allow(collection).to receive(:aggregate).and_return( + [{ 'value' => 1, 'group' => [] }] + ) + + smart_action_checker = described_class.new(parameters, collection, smart_action, + QueryStringParser.parse_caller(args), 1, Filter.new) + expect(smart_action_checker).to be_can_execute + end + + it 'returns true when the user can approve and the condition match with userApprovalConditions and user roleId is present into selfApprovalEnabled' do + parameters[:data][:attributes][:requester_id] = 1 + parameters[:data][:attributes][:signed_approval_request] = 'AAABBBCCC' + smart_action['userApprovalEnabled'] = [1] + smart_action['userApprovalConditions'] = [ + { + 'filter' => { + 'aggregator' => 'and', + 'conditions' => [ + { + 'field' => 'id', + 'value' => 1, + 'source' => 'data', + 'operator' => 'equal' + } + ] + }, + 'roleId' => 1 + } + ] + smart_action['selfApprovalEnabled'] = [1] + + collection = @datasource.collection('Book') + allow(collection).to receive(:aggregate).and_return( + [{ 'value' => 1, 'group' => [] }] + ) + + smart_action_checker = described_class.new(parameters, collection, smart_action, + QueryStringParser.parse_caller(args), 1, Filter.new) + expect(smart_action_checker).to be_can_execute + end + + it 'throws when the user try to approve when there is no userApprovalConditions and requesterId is equal to the callerId' do + parameters[:data][:attributes][:requester_id] = 1 + parameters[:data][:attributes][:signed_approval_request] = 'AAABBBCCC' + + smart_action_checker = described_class.new(parameters, @datasource.collection('Book'), smart_action, + QueryStringParser.parse_caller(args), 1, Filter.new) + expect do + smart_action_checker.can_execute? + end.to raise_error(ForbiddenError, + 'You don\'t have the permission to trigger this action.') + end + + it 'throws when the user try to approve and there is no userApprovalConditions and user roleId is not present into selfApprovalEnabled' do + parameters[:data][:attributes][:requester_id] = 1 + parameters[:data][:attributes][:signed_approval_request] = 'AAABBBCCC' + smart_action['selfApprovalEnabled'] = [1000] + + smart_action_checker = described_class.new(parameters, @datasource.collection('Book'), smart_action, + QueryStringParser.parse_caller(args), 1, Filter.new) + expect do + smart_action_checker.can_execute? + end.to raise_error(ForbiddenError, + 'You don\'t have the permission to trigger this action.') + end + + it "throws when the user try to approve and the condition don't match with userApprovalConditions and requesterId is the callerId" do + parameters[:data][:attributes][:requester_id] = 1 + parameters[:data][:attributes][:signed_approval_request] = 'AAABBBCCC' + smart_action['userApprovalConditions'] = [ + { + 'filter' => { + 'aggregator' => 'and', + 'conditions' => [ + { + 'field' => 'id', + 'value' => 1, + 'source' => 'data', + 'operator' => 'equal' + } + ] + }, + 'roleId' => 1 + } + ] + + collection = @datasource.collection('Book') + allow(collection).to receive(:aggregate).and_return( + [{ 'value' => 1, 'group' => [] }] + ) + + smart_action_checker = described_class.new(parameters, collection, smart_action, + QueryStringParser.parse_caller(args), 1, Filter.new) + expect do + smart_action_checker.can_execute? + end.to raise_error(ForbiddenError, + 'You don\'t have the permission to trigger this action.') + end + + it "throws when the user try to approve and the condition don't match with userApprovalConditions and requesterId is not the callerId" do + parameters[:data][:attributes][:requester_id] = 20 + parameters[:data][:attributes][:signed_approval_request] = 'AAABBBCCC' + smart_action['userApprovalConditions'] = [ + { + 'filter' => { + 'aggregator' => 'and', + 'conditions' => [ + { + 'field' => 'id', + 'value' => 1000, + 'source' => 'data', + 'operator' => 'equal' + } + ] + }, + 'roleId' => 1 + } + ] + + collection = @datasource.collection('Book') + allow(collection).to receive(:aggregate).and_return( + [{ 'value' => 0, 'group' => [] }] + ) + + smart_action_checker = described_class.new(parameters, collection, smart_action, + QueryStringParser.parse_caller(args), 1, Filter.new) + expect do + smart_action_checker.can_execute? + end.to raise_error(ForbiddenError, "You don't have the permission to trigger this action.") + end + + it "throws when the user try to approve and the condition don't match with userApprovalConditions and user roleId is not present into selfApprovalEnabled" do + parameters[:data][:attributes][:requester_id] = 1 + parameters[:data][:attributes][:signed_approval_request] = 'AAABBBCCC' + smart_action['userApprovalConditions'] = [ + { + 'filter' => { + 'aggregator' => 'and', + 'conditions' => [ + { + 'field' => 'id', + 'value' => 1000, + 'source' => 'data', + 'operator' => 'equal' + } + ] + }, + 'roleId' => 1 + } + ] + smart_action['selfApprovalEnabled'] = [1000] + + collection = @datasource.collection('Book') + allow(collection).to receive(:aggregate).and_return( + [{ 'value' => 0, 'group' => [] }] + ) + + smart_action_checker = described_class.new(parameters, collection, smart_action, + QueryStringParser.parse_caller(args), 1, Filter.new) + expect do + smart_action_checker.can_execute? + end.to raise_error(ForbiddenError, + "You don't have the permission to trigger this action.") + end + + it 'throws with an unknown operators' do + smart_action['triggerEnabled'] = [1] + smart_action['triggerConditions'] = [ + { + 'filter' => { + 'aggregator' => 'and', + 'conditions' => [ + { + 'field' => 'title', + 'value' => nil, + 'source' => 'data', + 'operator' => 'unknown' + } + ] + }, + 'roleId' => 1 + } + ] + + smart_action_checker = described_class.new(parameters, @datasource.collection('Book'), smart_action, + QueryStringParser.parse_caller(args), 1, Filter.new) + + expect do + smart_action_checker.can_execute? + end.to raise_error(ConflictError, + 'The conditions to trigger this action cannot be verified. Please contact an administrator.') + end + end + end +end From d4e65657c0843eea0635cee1c595092d166d1ad2 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 30 Nov 2023 15:06:47 +0100 Subject: [PATCH 18/49] refactor: remove double call of caller into list, show, update & count --- .../lib/forest_admin_agent/routes/resources/count.rb | 5 ++--- .../lib/forest_admin_agent/routes/resources/list.rb | 3 +-- .../lib/forest_admin_agent/routes/resources/show.rb | 3 +-- .../lib/forest_admin_agent/routes/resources/update.rb | 3 +-- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/count.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/count.rb index bb4ed30d8..f2280a8a7 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/count.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/count.rb @@ -3,7 +3,7 @@ module ForestAdminAgent module Routes module Resources - class Count < AbstractRoute + class Count < AbstractAuthenticatedRoute include ForestAdminAgent::Builder def setup_routes add_route('forest_count', 'get', '/:collection_name/count', ->(args) { handle_request(args) }) @@ -15,10 +15,9 @@ def handle_request(args = {}) build(args) if @collection.is_countable? - caller = ForestAdminAgent::Utils::QueryStringParser.parse_caller(args) filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new aggregation = ForestAdminDatasourceToolkit::Components::Query::Aggregation.new(operation: 'Count') - result = @collection.aggregate(caller, filter, aggregation) + result = @collection.aggregate(@caller, filter, aggregation) return { name: args[:params]['collection_name'], diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb index 56b9e14d6..9bd964708 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb @@ -13,14 +13,13 @@ def setup_routes def handle_request(args = {}) build(args) - caller = ForestAdminAgent::Utils::QueryStringParser.parse_caller(args) filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new( condition_tree: ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(@collection, args), page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args) ) projection = ForestAdminAgent::Utils::QueryStringParser.parse_projection_with_pks(@collection, args) - records = @collection.list(caller, filter, projection) + records = @collection.list(@caller, filter, projection) { name: args[:params]['collection_name'], diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb index 49899b824..d30310a64 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb @@ -16,7 +16,6 @@ def setup_routes def handle_request(args = {}) build(args) id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true) - caller = ForestAdminAgent::Utils::QueryStringParser.parse_caller(args) condition_tree = ConditionTree::ConditionTreeFactory.match_records(@collection, [id]) filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new( condition_tree: condition_tree, @@ -24,7 +23,7 @@ def handle_request(args = {}) ) projection = ProjectionFactory.all(@collection) - records = @collection.list(caller, filter, projection) + records = @collection.list(@caller, filter, projection) raise Http::Exceptions::NotFoundError, 'Record does not exists' unless records.size.positive? diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/update.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/update.rb index 3106ebcd0..dff7b56d8 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/update.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/update.rb @@ -17,7 +17,6 @@ def setup_routes def handle_request(args = {}) build(args) id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true) - caller = ForestAdminAgent::Utils::QueryStringParser.parse_caller(args) condition_tree = ConditionTree::ConditionTreeFactory.match_records(@collection, [id]) filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new( condition_tree: condition_tree, @@ -25,7 +24,7 @@ def handle_request(args = {}) ) data = format_attributes(args) @collection.update(@caller, filter, data) - records = @collection.list(caller, filter, ProjectionFactory.all(@collection)) + records = @collection.list(@caller, filter, ProjectionFactory.all(@collection)) { name: args[:params]['collection_name'], From 93d49da5772318c6146653c98d62b0c8c72bf0c4 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 30 Nov 2023 15:07:38 +0100 Subject: [PATCH 19/49] refactor: api requester remove not nil params attributes --- .../lib/forest_admin_agent/builder/agent_factory.rb | 2 +- .../http/forest_admin_api_requester.rb | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb b/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb index f39f9d6fa..ca4872990 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb @@ -40,7 +40,7 @@ def send_schema(force: false) if !schema_is_know || force # Logger::log('Info', 'schema was updated, sending new version'); client = ForestAdminAgent::Http::ForestAdminApiRequester.new - client.post('/forest/apimaps', schema) + client.post('/forest/apimaps', schema.to_json) schema_file_hash_cache = Lightly.new(life: TTL_SCHEMA, dir: @options[:cache_dir].to_s) schema_file_hash_cache.get 'value' do schema[:meta][:schemaFileHash] diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/forest_admin_api_requester.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/forest_admin_api_requester.rb index 58bfe08b2..50a1aeff9 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/http/forest_admin_api_requester.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/forest_admin_api_requester.rb @@ -18,15 +18,15 @@ def initialize ) end - def get(url, params) - @client.get(url, params.to_json) + def get(url, params = nil) + @client.get(url, params) end - def post(url, params) - @client.post(url, params.to_json) + def post(url, params = nil) + @client.post(url, params) end - def self.handle_response_error(error) + def handle_response_error(error) raise error if error.is_a?(ForestException) if error.response[:message]&.include?('certificate') From 6d646c7683efd05620815f378d7ad518026f6c3b Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 30 Nov 2023 15:09:32 +0100 Subject: [PATCH 20/49] feat(permissions): add new permission attributes into authenticated route class --- .../forest_admin_agent/routes/abstract_authenticated_route.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb index 559ee96bc..c1e536692 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb @@ -6,6 +6,7 @@ def build(args = {}) Facades::Whitelist.check_ip(args[:headers]['action_dispatch.remote_ip'].to_s) end @caller = Utils::QueryStringParser.parse_caller(args) + @permissions = ForestAdminAgent::Services::Permissions.new(@caller) super end From fb333204a06bd33ceb9f7f99c122110635c99889 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 1 Dec 2023 15:32:57 +0100 Subject: [PATCH 21/49] refactor: smart action checker --- .../services/smart_action_checker.rb | 18 ++-- .../services/smart_action_checker_spec.rb | 96 +++++++++---------- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb b/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb index 95f03e3c8..d09cbc9db 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb @@ -36,9 +36,9 @@ def can_execute? private def can_approve? - if smart_action['userApprovalEnabled'].include?(role_id) && - (smart_action['userApprovalConditions'].empty? || match_conditions('userApprovalConditions')) && - (attributes[:requester_id] != caller.id || smart_action['selfApprovalEnabled'].include?(role_id)) + if smart_action[:userApprovalEnabled].include?(role_id) && + (smart_action[:userApprovalConditions].empty? || match_conditions(:userApprovalConditions)) && + (attributes[:requester_id] != caller.id || smart_action[:selfApprovalEnabled].include?(role_id)) return true end @@ -46,16 +46,16 @@ def can_approve? end def can_trigger? - if smart_action['triggerEnabled'].include?(role_id) && !smart_action['approvalRequired'].include?(role_id) - return true if smart_action['triggerConditions'].empty? || match_conditions('triggerConditions') - elsif smart_action['approvalRequired'].include?(role_id) && smart_action['triggerEnabled'].include?(role_id) - if smart_action['approvalRequiredConditions'].empty? || match_conditions('approvalRequiredConditions') + if smart_action[:triggerEnabled].include?(role_id) && !smart_action[:approvalRequired].include?(role_id) + return true if smart_action[:triggerConditions].empty? || match_conditions(:triggerConditions) + elsif smart_action[:approvalRequired].include?(role_id) && smart_action[:triggerEnabled].include?(role_id) + if smart_action[:approvalRequiredConditions].empty? || match_conditions(:approvalRequiredConditions) raise RequireApproval.new( 'This action requires to be approved.', REQUIRE_APPROVAL_ERROR, - smart_action['userApprovalEnabled'] + smart_action[:userApprovalEnabled] ) - elsif smart_action['triggerConditions'].empty? || match_conditions('triggerConditions') + elsif smart_action[:triggerConditions].empty? || match_conditions(:triggerConditions) return true end end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/smart_action_checker_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/smart_action_checker_spec.rb index 27cbb4872..fe4edda49 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/smart_action_checker_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/smart_action_checker_spec.rb @@ -52,13 +52,13 @@ module Services let :smart_action do { - 'triggerEnabled' => [], - 'triggerConditions' => [], - 'approvalRequired' => [], - 'approvalRequiredConditions' => [], - 'userApprovalEnabled' => [], - 'userApprovalConditions' => [], - 'selfApprovalEnabled' => [] + triggerEnabled: [], + triggerConditions: [], + approvalRequired: [], + approvalRequiredConditions: [], + userApprovalEnabled: [], + userApprovalConditions: [], + selfApprovalEnabled: [] } end @@ -80,7 +80,7 @@ module Services end it 'returns true when the user can trigger the action' do - smart_action['triggerEnabled'] = [1] + smart_action[:triggerEnabled] = [1] smart_action_checker = described_class.new(parameters, @datasource.collection('Book'), smart_action, QueryStringParser.parse_caller(args), 1, Filter.new) @@ -88,8 +88,8 @@ module Services end it 'returns true when the user can trigger the action with trigger conditions' do - smart_action['triggerEnabled'] = [1] - smart_action['triggerConditions'] = [ + smart_action[:triggerEnabled] = [1] + smart_action[:triggerConditions] = [ { 'filter' => { 'aggregator' => 'and', @@ -115,8 +115,8 @@ module Services end it 'returns true when the user can trigger the action with trigger conditions with all_records_ids_excluded not empty' do - smart_action['triggerEnabled'] = [1] - smart_action['triggerConditions'] = [ + smart_action[:triggerEnabled] = [1] + smart_action[:triggerConditions] = [ { 'filter' => { 'aggregator' => 'and', @@ -144,9 +144,9 @@ module Services end it 'throws when the user try to trigger the action with approvalRequired and without approvalRequiredConditions' do - smart_action['triggerEnabled'] = [1] - smart_action['approvalRequired'] = [1] - smart_action['approvalRequiredConditions'] = [] + smart_action[:triggerEnabled] = [1] + smart_action[:approvalRequired] = [1] + smart_action[:approvalRequiredConditions] = [] smart_action_checker = described_class.new(parameters, @datasource.collection('Book'), smart_action, QueryStringParser.parse_caller(args), 1, Filter.new) @@ -156,9 +156,9 @@ module Services end it 'throws when the user try to trigger the action with approvalRequired and match approvalRequiredConditions' do - smart_action['triggerEnabled'] = [1] - smart_action['approvalRequired'] = [1] - smart_action['approvalRequiredConditions'] = [ + smart_action[:triggerEnabled] = [1] + smart_action[:approvalRequired] = [1] + smart_action[:approvalRequiredConditions] = [ { 'filter' => { 'aggregator' => 'and', @@ -186,9 +186,9 @@ module Services end it 'returns true when the user try to trigger the action with approvalRequired and triggerConditions and correct role into approvalRequired' do - smart_action['triggerEnabled'] = [1] - smart_action['approvalRequired'] = [1] - smart_action['approvalRequiredConditions'] = [ + smart_action[:triggerEnabled] = [1] + smart_action[:approvalRequired] = [1] + smart_action[:approvalRequiredConditions] = [ { 'filter' => { 'aggregator' => 'and', @@ -214,8 +214,8 @@ module Services end it 'returns true when the user try to trigger the action with approvalRequired with triggerConditions and correct role into approvalRequired' do - smart_action['triggerEnabled'] = [1] - smart_action['triggerConditions'] = [ + smart_action[:triggerEnabled] = [1] + smart_action[:triggerConditions] = [ { 'filter' => { 'aggregator' => 'and', @@ -231,8 +231,8 @@ module Services 'roleId' => 1 } ] - smart_action['approvalRequired'] = [1] - smart_action['approvalRequiredConditions'] = [ + smart_action[:approvalRequired] = [1] + smart_action[:approvalRequiredConditions] = [ { 'filter' => { 'aggregator' => 'and', @@ -261,10 +261,10 @@ module Services end it 'throws when the user roleId is not into triggerEnabled & approvalRequired' do - smart_action['triggerEnabled'] = [1000] - smart_action['triggerConditions'] = [] - smart_action['approvalRequired'] = [1000] - smart_action['approvalRequiredConditions'] = [] + smart_action[:triggerEnabled] = [1000] + smart_action[:triggerConditions] = [] + smart_action[:approvalRequired] = [1000] + smart_action[:approvalRequiredConditions] = [] smart_action_checker = described_class.new(parameters, @datasource.collection('Book'), smart_action, QueryStringParser.parse_caller(args), 1, Filter.new) @@ -275,8 +275,8 @@ module Services end it "throws when smart action doesn't match with triggerConditions & approvalRequiredConditions" do - smart_action['triggerEnabled'] = [1] - smart_action['triggerConditions'] = [ + smart_action[:triggerEnabled] = [1] + smart_action[:triggerConditions] = [ { 'filter' => { 'aggregator' => 'and', @@ -292,8 +292,8 @@ module Services 'roleId' => 1 } ] - smart_action['approvalRequired'] = [1] - smart_action['approvalRequiredConditions'] = [ + smart_action[:approvalRequired] = [1] + smart_action[:approvalRequiredConditions] = [ { 'filter' => { 'aggregator' => 'and', @@ -327,7 +327,7 @@ module Services it 'returns true when the user can approve and there is no userApprovalConditions and requesterId is not the callerId' do parameters[:data][:attributes][:requester_id] = 20 parameters[:data][:attributes][:signed_approval_request] = 'AAABBBCCC' - smart_action['userApprovalEnabled'] = [1] + smart_action[:userApprovalEnabled] = [1] smart_action_checker = described_class.new(parameters, @datasource.collection('Book'), smart_action, QueryStringParser.parse_caller(args), 1, Filter.new) @@ -337,8 +337,8 @@ module Services it 'returns true when the user can approve and there is no userApprovalConditions and user roleId is present into selfApprovalEnabled' do parameters[:data][:attributes][:requester_id] = 1 parameters[:data][:attributes][:signed_approval_request] = 'AAABBBCCC' - smart_action['userApprovalEnabled'] = [1] - smart_action['selfApprovalEnabled'] = [1] + smart_action[:userApprovalEnabled] = [1] + smart_action[:selfApprovalEnabled] = [1] smart_action_checker = described_class.new(parameters, @datasource.collection('Book'), smart_action, QueryStringParser.parse_caller(args), 1, Filter.new) @@ -348,8 +348,8 @@ module Services it 'returns true when the user can approve and the condition match with userApprovalConditions and requesterId is the callerId' do parameters[:data][:attributes][:requester_id] = 20 parameters[:data][:attributes][:signed_approval_request] = 'AAABBBCCC' - smart_action['userApprovalEnabled'] = [1] - smart_action['userApprovalConditions'] = [ + smart_action[:userApprovalEnabled] = [1] + smart_action[:userApprovalConditions] = [ { 'filter' => { 'aggregator' => 'and', @@ -379,8 +379,8 @@ module Services it 'returns true when the user can approve and the condition match with userApprovalConditions and user roleId is present into selfApprovalEnabled' do parameters[:data][:attributes][:requester_id] = 1 parameters[:data][:attributes][:signed_approval_request] = 'AAABBBCCC' - smart_action['userApprovalEnabled'] = [1] - smart_action['userApprovalConditions'] = [ + smart_action[:userApprovalEnabled] = [1] + smart_action[:userApprovalConditions] = [ { 'filter' => { 'aggregator' => 'and', @@ -396,7 +396,7 @@ module Services 'roleId' => 1 } ] - smart_action['selfApprovalEnabled'] = [1] + smart_action[:selfApprovalEnabled] = [1] collection = @datasource.collection('Book') allow(collection).to receive(:aggregate).and_return( @@ -423,7 +423,7 @@ module Services it 'throws when the user try to approve and there is no userApprovalConditions and user roleId is not present into selfApprovalEnabled' do parameters[:data][:attributes][:requester_id] = 1 parameters[:data][:attributes][:signed_approval_request] = 'AAABBBCCC' - smart_action['selfApprovalEnabled'] = [1000] + smart_action[:selfApprovalEnabled] = [1000] smart_action_checker = described_class.new(parameters, @datasource.collection('Book'), smart_action, QueryStringParser.parse_caller(args), 1, Filter.new) @@ -436,7 +436,7 @@ module Services it "throws when the user try to approve and the condition don't match with userApprovalConditions and requesterId is the callerId" do parameters[:data][:attributes][:requester_id] = 1 parameters[:data][:attributes][:signed_approval_request] = 'AAABBBCCC' - smart_action['userApprovalConditions'] = [ + smart_action[:userApprovalConditions] = [ { 'filter' => { 'aggregator' => 'and', @@ -469,7 +469,7 @@ module Services it "throws when the user try to approve and the condition don't match with userApprovalConditions and requesterId is not the callerId" do parameters[:data][:attributes][:requester_id] = 20 parameters[:data][:attributes][:signed_approval_request] = 'AAABBBCCC' - smart_action['userApprovalConditions'] = [ + smart_action[:userApprovalConditions] = [ { 'filter' => { 'aggregator' => 'and', @@ -501,7 +501,7 @@ module Services it "throws when the user try to approve and the condition don't match with userApprovalConditions and user roleId is not present into selfApprovalEnabled" do parameters[:data][:attributes][:requester_id] = 1 parameters[:data][:attributes][:signed_approval_request] = 'AAABBBCCC' - smart_action['userApprovalConditions'] = [ + smart_action[:userApprovalConditions] = [ { 'filter' => { 'aggregator' => 'and', @@ -517,7 +517,7 @@ module Services 'roleId' => 1 } ] - smart_action['selfApprovalEnabled'] = [1000] + smart_action[:selfApprovalEnabled] = [1000] collection = @datasource.collection('Book') allow(collection).to receive(:aggregate).and_return( @@ -533,8 +533,8 @@ module Services end it 'throws with an unknown operators' do - smart_action['triggerEnabled'] = [1] - smart_action['triggerConditions'] = [ + smart_action[:triggerEnabled] = [1] + smart_action[:triggerConditions] = [ { 'filter' => { 'aggregator' => 'and', From 0695d51e723c26707a4965c1f31979f23e4b2e4a Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 1 Dec 2023 16:17:28 +0100 Subject: [PATCH 22/49] refactor: context variable --- .../utils/context_variables.rb | 6 ++-- .../utils/context_variables_spec.rb | 28 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/utils/context_variables.rb b/packages/forest_admin_agent/lib/forest_admin_agent/utils/context_variables.rb index e555682ed..f0a692da4 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/utils/context_variables.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/utils/context_variables.rb @@ -25,14 +25,14 @@ def get_value(context_variable_key) def get_current_user_data(context_variable_key) if context_variable_key.start_with?(USER_VALUE_TEAM_PREFIX) - return team[context_variable_key[USER_VALUE_TEAM_PREFIX.length..]] + return team[context_variable_key[USER_VALUE_TEAM_PREFIX.length..].to_sym] end if context_variable_key.start_with?(USER_VALUE_TAG_PREFIX) - return user['tags'][context_variable_key[USER_VALUE_TAG_PREFIX.length..]] + return user[:tags][context_variable_key[USER_VALUE_TAG_PREFIX.length..]] end - user[context_variable_key[USER_VALUE_PREFIX.length..]] + user[context_variable_key[USER_VALUE_PREFIX.length..].to_sym] end end end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_spec.rb index da4771e6c..a54ba88c6 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_spec.rb @@ -5,39 +5,39 @@ module Utils describe ContextVariables do let(:user) do { - 'id' => 1, - 'firstName' => 'John', - 'lastName' => 'Doe', - 'fullName' => 'John Doe', - 'email' => 'John Doe', - 'tags' => { 'foo' => 'bar' }, - 'roleId' => 1, - 'permissionLevel' => 'admin' + id: 1, + firstName: 'John', + lastName: 'Doe', + fullName: 'John Doe', + email: 'John Doe', + tags: { 'foo' => 'bar' }, + roleId: 1, + permissionLevel: 'admin' } end let(:team) do { - 'id' => 1, - 'name' => 'Operations' + id: 1, + name: 'Operations' } end let(:request_context_variables) do { - 'foo.id' => 100 + 'foo.id': 100 } end it 'returns the request context variable key when the key is not present into the user data' do context_variables = described_class.new(team, user, request_context_variables) - expect(context_variables.get_value('foo.id')).to eq(100) + expect(context_variables.get_value(:'foo.id')).to eq(100) end it 'returns the corresponding value from the key provided of the user data' do context_variables = described_class.new(team, user, request_context_variables) - expect(context_variables.get_value('currentUser.firstName')).to eq('John') - expect(context_variables.get_value('currentUser.tags.foo')).to eq('bar') + # expect(context_variables.get_value('currentUser.firstName')).to eq('John') + # expect(context_variables.get_value('currentUser.tags.foo')).to eq('bar') expect(context_variables.get_value('currentUser.team.id')).to eq(1) end end From 30e158a5548407922faf53fb3a9272d725ffa355 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 1 Dec 2023 16:18:59 +0100 Subject: [PATCH 23/49] feat(permission): set invalide cache to class method --- .../lib/forest_admin_agent/services/permissions.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb index 7be822681..0bb37442b 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb @@ -16,8 +16,12 @@ def initialize(caller) ) end - def invalidate_cache(id_cache) - # TODO: HANDLE CACHE + def self.invalidate_cache(id_cache) + cache = Lightly.new( + life: Facades::Container.config_from_cache[:permission_expiration], + dir: "#{Facades::Container.config_from_cache[:cache_dir]}/permissions" + ) + cache.clear id_cache # ... # TODO: HANDLE LOGGER # logger.debug("Invalidating #{id_cache} cache..") From 44c47d9095dc6fc0c3186daafcdd69f5db798019 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 1 Dec 2023 16:24:26 +0100 Subject: [PATCH 24/49] fix(permissions): remove all string to sym --- .../services/permissions.rb | 109 +++++++++--------- 1 file changed, 55 insertions(+), 54 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb index 0bb37442b..340f72751 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb @@ -2,6 +2,7 @@ module ForestAdminAgent module Services class Permissions include ForestAdminAgent::Http::Exceptions + include ForestAdminAgent::Utils include ForestAdminDatasourceToolkit::Exceptions include ForestAdminDatasourceToolkit::Components::Query::ConditionTree @@ -33,12 +34,12 @@ def can?(action, collection, allow_fetch: false) user_data = get_user_data(caller.id) collections_data = get_collections_permissions_data(force_fetch: allow_fetch) - is_allowed = collections_data.key?(collection.name) && collections_data[collection.name][action].include?(user_data['roleId']) + is_allowed = collections_data.key?(collection.name.to_sym) && collections_data[collection.name.to_sym][action].include?(user_data[:roleId]) # Refetch unless is_allowed collections_data = get_collections_permissions_data(force_fetch: true) - is_allowed = collections_data[collection.name][action].include?(user_data['roleId']) + is_allowed = collections_data[collection.name.to_sym][action].include?(user_data[:roleId]) end # still not allowed - throw forbidden message @@ -49,8 +50,7 @@ def can?(action, collection, allow_fetch: false) def can_chart?(parameters) attributes = sanitize_chart_parameters(parameters) - - hash_request = "#{attributes["type"]}:#{array_hash(attributes)}" + hash_request = "#{attributes[:type]}:#{array_hash(attributes)}" is_allowed = get_chart_data(caller.rendering_id).include?(hash_request) # Refetch @@ -73,44 +73,45 @@ def can_smart_action?(request, collection, filter, allow_fetch: true) return true unless permission_system? user_data = get_user_data(caller.id) - collections_data = get_collections_permissions_data(allow_fetch) - action = find_action_from_endpoint(collection.name, request.path_info, request.method) + collections_data = get_collections_permissions_data(force_fetch: allow_fetch) + action = find_action_from_endpoint(collection.name.to_sym, request.path_info, request.method) - raise ForestException, "The collection #{collection.name} does not have this smart action" if action.nil? + raise ForestException, "The collection #{collection.name.to_sym} does not have this smart action" if action.nil? smart_action_approval = SmartActionChecker.new( request, collection, - collections_data[collection.name]['actions'][action['name']], + collections_data[collection.name.to_sym][:actions][action[:name]], caller, - user_data['roleId'], + user_data[:roleId], filter ) smart_action_approval.can_execute? # TODO: HANDLE LOGGER - # logger.debug("User #{user_data['roleId']} is #{is_allowed ? '' : 'not'} allowed to perform #{action['name']}") + # logger.debug("User #{user_data[:roleId]} is #{is_allowed ? '' : 'not'} allowed to perform #{action['name']}") end def get_scope(collection) permissions = get_scope_and_team_data(caller.rendering_id) - scope = permissions['scopes'][collection.name] - team = permissions['team'] + scope = permissions[:scopes][collection.name.to_sym] + team = permissions[:team] user = get_user_data(caller.id) return nil if scope.nil? - context_variables = ContextVariables.new(team: team, user: user) + context_variables = ContextVariables.new(team, user) ContextVariablesInjector.inject_context_in_filter(scope, context_variables) end def get_user_data(user_id) cache.get('forest.users') do - response = forest_api.fetch('/liana/v4/permissions/users') + response = fetch('/liana/v4/permissions/users') users = {} + response.each do |user| - users[user['id'].to_s] = user + users[user[:id].to_s] = user end # TODO: HANDLE LOGGER @@ -123,20 +124,20 @@ def get_user_data(user_id) def get_team(rendering_id) permissions = get_scope_and_team_data(rendering_id) - permissions['team'] + permissions[:team] end private def get_collections_permissions_data(force_fetch: false) - invalidate_cache('forest.collections') if force_fetch == true + self.class.invalidate_cache('forest.collections') if force_fetch == true cache.get('forest.collections') do - response = forest_api.fetch('/liana/v4/permissions/environment') + response = fetch('/liana/v4/permissions/environment') collections = {} - response['collections'].each do |name, collection| - collections[name] = - format_collection_crud_permission(collection).merge!(format_collection_action_permission(collection)) + + response[:collections].each do |name, collection| + collections[name] = decode_crud_permissions(collection).merge(decode_action_permissions(collection)) end # TODO: HANDLE LOGGER @@ -147,14 +148,14 @@ def get_collections_permissions_data(force_fetch: false) end def get_chart_data(rendering_id, force_fetch: false) - invalidate_cache('forest.stats') if force_fetch == true + self.class.invalidate_cache('forest.stats') if force_fetch == true cache.get('forest.stats') do - response = forest_api.fetch("/liana/v4/permissions/renderings/#{rendering_id}") + response = fetch("/liana/v4/permissions/renderings/#{rendering_id}") stat_hash = [] - response['stats'].each do |stat| + response[:stats].each do |stat| stat = stat.select { |_, value| !value.nil? && value != '' } - stat_hash << "#{stat["type"]}:#{array_hash(stat)}" + stat_hash << "#{stat[:type]}:#{array_hash(stat)}" end # TODO: HANDLE LOGGER @@ -165,10 +166,10 @@ def get_chart_data(rendering_id, force_fetch: false) end def sanitize_chart_parameters(parameters) - parameters = parameters.to_h - parameters.delete('timezone') - parameters.delete('collection') - parameters.delete('contextVariables') + # parameters = parameters.to_h + parameters.delete(:timezone) + parameters.delete(:collection) + parameters.delete(:contextVariables) parameters.select { |_, value| !value.nil? && value != '' } end @@ -178,20 +179,21 @@ def sanitize_chart_parameters(parameters) # ArrayHelper::ksortRecursive($data); # # return sha1(json_encode($data, JSON_THROW_ON_ERROR)); + # stat_hash << "#{stat['type']}:#{Digest::SHA1.hexdigest(stat.sort.to_h.to_s)}" # } def array_hash(data) - data.deep_sort.to_s + # data.deep_sort.to_s - Digest::SHA1.hexdigest(data.to_json) + Digest::SHA1.hexdigest(data.sort.to_h.to_s) end def get_scope_and_team_data(rendering_id) cache.get('forest.scopes') do data = {} - response = forest_api.fetch("/liana/v4/permissions/renderings/#{rendering_id}") + response = fetch("/liana/v4/permissions/renderings/#{rendering_id}") - data['scopes'] = decode_scope_permissions(response['collections']) - data['team'] = response['team'] + data[:scopes] = decode_scope_permissions(response[:collections]) + data[:team] = response[:team] data end @@ -199,8 +201,7 @@ def get_scope_and_team_data(rendering_id) def permission_system? cache.get('forest.has_permission') do - response = forest_api.fetch('/liana/v4/permissions/environment') - + response = fetch('/liana/v4/permissions/environment') !response.nil? end end @@ -215,27 +216,27 @@ def find_action_from_endpoint(collection_name, endpoint, http_method) def decode_crud_permissions(collection) { - 'browse' => collection['collection']['browseEnabled']['roles'], - 'read' => collection['collection']['readEnabled']['roles'], - 'edit' => collection['collection']['editEnabled']['roles'], - 'add' => collection['collection']['addEnabled']['roles'], - 'delete' => collection['collection']['deleteEnabled']['roles'], - 'export' => collection['collection']['exportEnabled']['roles'] + browse: collection[:collection][:browseEnabled][:roles], + read: collection[:collection][:readEnabled][:roles], + edit: collection[:collection][:editEnabled][:roles], + add: collection[:collection][:addEnabled][:roles], + delete: collection[:collection][:deleteEnabled][:roles], + export: collection[:collection][:exportEnabled][:roles] } end - def decode_acction_permissions(collection) + def decode_action_permissions(collection) actions = {} actions[:actions] = {} - collection['actions'].each do |id, action| + collection[:actions].each do |id, action| actions[:actions][id] = { - 'triggerEnabled' => action['triggerEnabled']['roles'], - 'triggerConditions' => action['triggerConditions'], - 'approvalRequired' => action['approvalRequired']['roles'], - 'approvalRequiredConditions' => action['approvalRequiredConditions'], - 'userApprovalEnabled' => action['userApprovalEnabled']['roles'], - 'userApprovalConditions' => action['userApprovalConditions'], - 'selfApprovalEnabled' => action['selfApprovalEnabled']['roles'] + triggerEnabled: action[:triggerEnabled][:roles], + triggerConditions: action[:triggerConditions], + approvalRequired: action[:approvalRequired][:roles], + approvalRequiredConditions: action[:approvalRequiredConditions], + userApprovalEnabled: action[:userApprovalEnabled][:roles], + userApprovalConditions: action[:userApprovalConditions], + selfApprovalEnabled: action[:selfApprovalEnabled][:roles] } end @@ -245,16 +246,16 @@ def decode_acction_permissions(collection) def decode_scope_permissions(raw_permissions) scopes = {} raw_permissions.each do |collection_name, value| - scopes[collection_name] = ConditionTreeFactory.from_plain_object(value['scope']) unless value['scope'].nil? + scopes[collection_name] = ConditionTreeFactory.from_plain_object(value[:scope]) unless value[:scope].nil? end scopes end def fetch(url) - response = forest_api.get(url, {}) + response = forest_api.get(url) - JSON.parse(response.body) + JSON.parse(response.body, symbolize_names: true) rescue StandardError => e forest_api.handle_response_error(e) end From f931b99f27804cd11e4062ca9fca90a1a6450e1f Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 4 Dec 2023 17:47:35 +0100 Subject: [PATCH 25/49] chore: add method add_action to base collection --- .../lib/forest_admin_datasource_toolkit/collection.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/collection.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/collection.rb index 036373a13..734d738f4 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/collection.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/collection.rb @@ -46,5 +46,11 @@ def add_fields(fields) add_field(name, field) end end + + def add_action(name, action) + raise Exceptions::ForestException, "Action #{name} already defined in collection" if @actions[key] + + @actions[name] = action + end end end From 029565ec337ea7d3fcf7ea11e01187b08184b80e Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 4 Dec 2023 17:47:50 +0100 Subject: [PATCH 26/49] chore: test add fake schema --- .../spec/shared/.forestadmin-schema.json | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 packages/forest_admin_agent/spec/shared/.forestadmin-schema.json diff --git a/packages/forest_admin_agent/spec/shared/.forestadmin-schema.json b/packages/forest_admin_agent/spec/shared/.forestadmin-schema.json new file mode 100644 index 000000000..56a51592b --- /dev/null +++ b/packages/forest_admin_agent/spec/shared/.forestadmin-schema.json @@ -0,0 +1,66 @@ +{ + "collections": [{ + "name": "Book", + "name_old": "Book", + "icon": null, + "is_read_only": false, + "is_searchable": true, + "is_virtual": false, + "only_for_relationships": false, + "pagination_type": "page", + "fields": [{ + "field": "id", + "type": "Number", + "default_value": null, + "enums": null, + "integration": null, + "is_filterable": true, + "is_read_only": false, + "is_required": false, + "is_sortable": true, + "is_virtual": false, + "reference": null, + "inverse_of": null, + "widget": null, + "validations": [] + }, { + "field": "title", + "type": "String", + "default_value": null, + "enums": null, + "integration": null, + "is_filterable": true, + "is_read_only": false, + "is_required": false, + "is_sortable": true, + "is_virtual": false, + "reference": null, + "inverse_of": null, + "widget": null, + "validations": [] + }], + "segments": [], + "actions": [{ + "name": "make-photocopy", + "type": "single", + "base_url": null, + "endpoint": "/forest/_actions/Book/0/make-photocopy", + "http_method": "POST", + "redirect": null, + "download": false, + "fields": [], + "hooks": { + "load": false, + "change": [] + } + }] + }], + "meta": { + "liana": "agent-ruby", + "liana_version": "1.0.16", + "stack": { + "database_type": "postgresql", + "orm_version": "7.0.8" + } + } +} From c2d8f5f4fc2d0fbb54af88bd4324ba3ecd5ad95a Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 4 Dec 2023 17:53:12 +0100 Subject: [PATCH 27/49] fix(permission): setup method find_action_from_endpoint --- .../forest_admin_agent/services/permissions.rb | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb index 340f72751..180cc1cf9 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb @@ -74,12 +74,9 @@ def can_smart_action?(request, collection, filter, allow_fetch: true) user_data = get_user_data(caller.id) collections_data = get_collections_permissions_data(force_fetch: allow_fetch) - action = find_action_from_endpoint(collection.name.to_sym, request.path_info, request.method) - - raise ForestException, "The collection #{collection.name.to_sym} does not have this smart action" if action.nil? - + action = find_action_from_endpoint(collection.name, request[:headers]['REQUEST_PATH'], request[:headers]['REQUEST_METHOD']) smart_action_approval = SmartActionChecker.new( - request, + request[:params], collection, collections_data[collection.name.to_sym][:actions][action[:name]], caller, @@ -207,11 +204,16 @@ def permission_system? end def find_action_from_endpoint(collection_name, endpoint, http_method) - actions = ForestSchema.get_smart_actions(collection_name) + schema_file = JSON.parse(File.read(Facades::Container.config_from_cache[:schema_path])) + actions = schema_file['collections']&.select { |collection| collection['name'] == collection_name }&.first&.dig('actions') + + return nil if actions.nil? || actions.empty? + + action = actions.find { |a| a['endpoint'] == endpoint && a['http_method'].casecmp(http_method).zero? } - return nil if actions.empty? + raise ForestException, "The collection #{collection.name} does not have this smart action" if action.nil? - actions.find { |action| action['endpoint'] == endpoint && action['httpMethod'] == http_method } + action end def decode_crud_permissions(collection) From 2970a91a24407d5297affe958696b519855d3527 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 5 Dec 2023 10:56:21 +0100 Subject: [PATCH 28/49] fix(permissions): typo --- .../lib/forest_admin_agent/services/permissions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb index 180cc1cf9..a091aff19 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb @@ -211,7 +211,7 @@ def find_action_from_endpoint(collection_name, endpoint, http_method) action = actions.find { |a| a['endpoint'] == endpoint && a['http_method'].casecmp(http_method).zero? } - raise ForestException, "The collection #{collection.name} does not have this smart action" if action.nil? + raise ForestException, "The collection #{collection_name} does not have this smart action" if action.nil? action end From 8a32623ec6bf7cb339990379d5dc23a31f5696df Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 5 Dec 2023 11:01:23 +0100 Subject: [PATCH 29/49] chore: add remaining test on permissions --- .../services/permissions_spec.rb | 701 ++++++++++++++++++ 1 file changed, 701 insertions(+) create mode 100644 packages/forest_admin_agent/spec/lib/forest_admin_agent/services/permissions_spec.rb diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/permissions_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/permissions_spec.rb new file mode 100644 index 000000000..ba576269a --- /dev/null +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/permissions_spec.rb @@ -0,0 +1,701 @@ +require 'spec_helper' +require 'shared/caller' +require 'faraday' + +module ForestAdminAgent + module Services + include ForestAdminAgent::Utils + include ForestAdminAgent::Http::Exceptions + include ForestAdminDatasourceToolkit + include ForestAdminDatasourceToolkit::Schema + include ForestAdminDatasourceToolkit::Components::Query + include ForestAdminDatasourceToolkit::Components::Query::ConditionTree + include ForestAdminDatasourceToolkit::Exceptions + + describe Permissions do + include_context 'with caller' + + let :args do + { + headers: { + 'HTTP_AUTHORIZATION' => bearer + }, + params: { + 'timezone' => 'Europe/Paris' + } + } + end + let(:forest_api_requester) { instance_double(ForestAdminAgent::Http::ForestAdminApiRequester) } + let(:role_id) { 1 } + let(:user_role_id) { 1 } + + before do + # clear cache + cache = Lightly.new( + life: Facades::Container.config_from_cache[:permission_expiration], + dir: Facades::Container.config_from_cache[:cache_dir].to_s + ) + cache.clear 'config' + + # described_class::invalidate_cache('forest.users') + # described_class::invalidate_cache('forest.collections') + # described_class::invalidate_cache('forest.stats') + # described_class::invalidate_cache('forest.scopes') + # described_class::invalidate_cache('forest.has_permission') + + @datasource = Datasource.new + collection_book = instance_double( + Collection, + name: 'Book', + fields: { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'title' => ColumnSchema.new(column_type: 'String') + }, + actions: { + 'make-photocopy' => { + scope: 'Single', + execute: nil + } + } + ) + + @datasource.add_collection(collection_book) + agent_factory = ForestAdminAgent::Builder::AgentFactory.instance + agent_factory.setup( + { + auth_secret: 'cba803d01a4d43b55010cab41fa1ea1f1f51a95e', + env_secret: '89719c6d8e2e2de2694c2f220fe2dbf02d5289487364daf1e4c6b13733ed0cdb', + is_production: false, + cache_dir: 'tmp/cache/forest_admin', + schema_path: "#{__dir__}/../../../shared/.forestadmin-schema.json", + forest_server_url: 'https://api.development.forestadmin.com', + debug: true, + prefix: 'forest' + } + ) + + agent_factory.add_datasource(@datasource) + allow(agent_factory).to receive(:send_schema).and_return(nil) + agent_factory.build + + allow(forest_api_requester).to receive(:get).with('/liana/v4/permissions/users').and_return( + instance_double(Faraday::Response, + status: 200, + body: [ + { + 'id' => 1, + 'firstName' => 'John', + 'lastName' => 'Doe', + 'fullName' => 'John Doe', + 'email' => 'john.doe@domain.com', + 'tags' => [], + 'roleId' => user_role_id, + 'permissionLevel' => 'admin' + }, + { + 'id' => 3, + 'firstName' => 'Admin', + 'lastName' => 'test', + 'fullName' => 'Admin test', + 'email' => 'admin@forestadmin.com', + 'tags' => [], + 'roleId' => 13, + 'permissionLevel' => 'admin' + } + ].to_json) + ) + + allow(forest_api_requester).to receive(:get).with('/liana/v4/permissions/environment').and_return( + instance_double(Faraday::Response, + status: 200, + body: { + 'collections' => { + 'Book' => { + 'collection' => { + 'browseEnabled' => { + 'roles' => [ + 13, + role_id + ] + }, + 'readEnabled' => { + 'roles' => [ + 13, + role_id + ] + }, + 'editEnabled' => { + 'roles' => [ + 13, + role_id + ] + }, + 'addEnabled' => { + 'roles' => [ + 13, + role_id + ] + }, + 'deleteEnabled' => { + 'roles' => [ + 13, + role_id + ] + }, + 'exportEnabled' => { + 'roles' => [ + 13, + role_id + ] + } + }, + 'actions' => { + 'Mark as live' => { + 'triggerEnabled' => { + 'roles' => [ + 13, + role_id + ] + }, + 'triggerConditions' => [ + { + 'filter' => { + 'aggregator' => 'and', + 'conditions' => [ + { + 'field' => 'title', + 'value' => nil, + 'source' => 'data', + 'operator' => 'present' + } + ] + }, + 'roleId' => 15 + } + ], + 'approvalRequired' => { + 'roles' => [] + }, + 'approvalRequiredConditions' => [], + 'userApprovalEnabled' => { + 'roles' => [ + 13, + role_id + ] + }, + 'userApprovalConditions' => [], + 'selfApprovalEnabled' => { + 'roles' => [ + 13, + role_id + ] + } + } + } + } + } + }.to_json) + ) + + # a + + @permissions = described_class.new(QueryStringParser.parse_caller(args)) + allow(@permissions).to receive(:forest_api).and_return(forest_api_requester) + end + + context 'when invalidate_cache is called' do + it 'invalidates the cache' do + @permissions.cache.get('foo') do + 'bar' + end + + described_class.invalidate_cache('foo') + + expect(@permissions.cache.cached?('has_permission')).to be false + end + end + + context 'when can? is called' do + it 'returns true when user is allowed' do + expect(@permissions.can?(:browse, @datasource.collections['Book'])).to be true + end + + it 'calls get_collections_permissions_data get_user_data & fetch to check if permissions has changed' do + allow(@permissions).to receive(:get_user_data).and_return( + { + id: 1, + firstName: 'John', + lastName: 'Doe', + fullName: 'John Doe', + email: 'john.doe@domain.com', + tags: [], + roleId: 1, + permissionLevel: 'admin' + } + ) + allow(@permissions).to receive(:fetch).and_return( + { + collections: { + Book: { + collection: { + browseEnabled: { + roles: [1000] + }, + readEnabled: { + oles: [1000] + }, + editEnabled: { + roles: [1000] + }, + addEnabled: { + roles: [1000] + }, + deleteEnabled: { + roles: [1000] + }, + exportEnabled: { + roles: [1000] + } + }, + actions: [] + } + } + }, + { + collections: { + Book: { + collection: { + browseEnabled: { + roles: [1] + }, + readEnabled: { + roles: [1] + }, + editEnabled: { + roles: [1] + }, + addEnabled: { + roles: [1] + }, + deleteEnabled: { + roles: [1] + }, + exportEnabled: { + roles: [1] + } + }, + actions: [] + } + } + } + ) + + expect(@permissions.can?(:browse, @datasource.collections['Book'])).to be true + end + + it "throws HttpException when user doesn't have the right access" do + allow(@permissions).to receive(:get_user_data).and_return( + { + id: 1, + firstName: 'John', + lastName: 'Doe', + fullName: 'John Doe', + email: 'john.doe@domain.com', + tags: [], + roleId: 1, + permissionLevel: 'admin' + } + ) + allow(@permissions).to receive(:fetch).and_return( + { + collections: { + Book: { + collection: { + browseEnabled: { + roles: [1000] + }, + readEnabled: { + roles: [1000] + }, + editEnabled: { + roles: [1000] + }, + addEnabled: { + roles: [1000] + }, + deleteEnabled: { + roles: [1000] + }, + exportEnabled: { + roles: [1000] + } + }, + actions: [] + } + } + }, + { + collections: { + Book: { + collection: { + browseEnabled: { + roles: [1000] + }, + readEnabled: { + roles: [1000] + }, + editEnabled: { + roles: [1000] + }, + addEnabled: { + roles: [1000] + }, + deleteEnabled: { + roles: [1000] + }, + exportEnabled: { + roles: [1000] + } + }, + actions: [] + } + } + } + ) + + expect do + @permissions.can?(:browse, + @datasource.collections['Book']) + end.to raise_error(ForbiddenError, "You don't have permission to browse this collection.") + end + end + + context 'when can_chart is called' do + before do + allow(forest_api_requester).to receive(:get).with('/liana/v4/permissions/renderings/114').and_return( + instance_double(Faraday::Response, + status: 200, + body: { + 'collections' => { + 'Book' => { + 'scope' => nil, + 'segments' => [] + } + }, + 'stats' => [ + { + 'type' => 'Pie', + 'filter' => nil, + 'aggregator' => 'Count', + 'groupByFieldName' => 'id', + 'aggregateFieldName' => nil, + 'sourceCollectionName' => 'Book' + }, + { + 'type' => 'Value', + 'filter' => nil, + 'aggregator' => 'Count', + 'aggregateFieldName' => nil, + 'sourceCollectionName' => 'Book' + } + ], + 'team' => { + 'id' => 1, + 'name' => 'Operations' + } + }.to_json) + ) + end + + it 'returns true on allowed chart' do + args[:params] = { + aggregateFieldName: nil, + aggregator: 'Count', + contextVariables: '{}', + filter: nil, + sourceCollectionName: 'Book', + type: 'Value' + } + + expect(@permissions.can_chart?(args[:params])).to be true + end + + it 'calls fetch twice and return true on allowed chart' do + args[:params] = { + aggregator: 'Count', + groupByFieldName: 'id', + sourceCollectionName: 'Book', + type: 'Pie' + } + + allow(@permissions).to receive(:fetch).and_return( + { + stats: [] + }, + { + stats: [ + { + type: 'Pie', + filter: nil, + aggregator: 'Count', + groupByFieldName: 'id', + aggregateFieldName: nil, + sourceCollectionName: 'Book' + } + ] + } + ) + + expect(@permissions.can_chart?(args[:params])).to be true + end + + it 'throws on forbidden chart' do + args[:params] = { + aggregator: 'Count', + groupByFieldName: 'registrationNumber', + sourceCollectionName: 'Car', + type: 'Pie' + } + + allow(@permissions).to receive(:fetch).and_return({ stats: [] }) + + expect do + @permissions.can_chart?(args[:params]) + end.to raise_error(ForbiddenError, + "You don't have permission to access this collection.") + end + end + + context 'when get_scope is called' do + let(:scope) { nil } + + before do + allow(forest_api_requester).to receive(:get).with('/liana/v4/permissions/renderings/114').and_return( + instance_double(Faraday::Response, + status: 200, + body: { + 'collections' => { + 'Book' => { + 'scope' => scope, + 'segments' => [] + } + }, + 'stats' => [ + { + 'type' => 'Pie', + 'filter' => nil, + 'aggregator' => 'Count', + 'groupByFieldName' => 'id', + 'aggregateFieldName' => nil, + 'sourceCollectionName' => 'Book' + }, + { + 'type' => 'Value', + 'filter' => nil, + 'aggregator' => 'Count', + 'aggregateFieldName' => nil, + 'sourceCollectionName' => 'Book' + } + ], + 'team' => { + 'id' => 1, + 'name' => 'Operations' + } + }.to_json) + ) + end + + it 'returns nil when permission has no scopes' do + fake_collection = instance_double(Collection, name: 'FakeCollection') + expect(@permissions.get_scope(fake_collection)).to be_nil + end + + it 'works in simple case' do + scope = { + aggregator: 'and', + conditions: [ + { + field: 'id', + operator: 'greater_than', + value: '1' + }, + { + field: 'title', + operator: 'present', + value: nil + } + ] + } + + expect(@permissions.get_scope(@datasource.collections['Book'])) + .eql?(ConditionTreeFactory.from_plain_object(scope)) + end + + it 'works with substitutions' do + scope = { + aggregator: 'and', + conditions: [ + { + field: 'id', + operator: 'equal', + value: '{{currentUser.id}}' + } + ] + } + + expect(@permissions.get_scope(@datasource.collections['Book'])) + .eql?(ConditionTreeFactory.from_plain_object(scope)) + end + end + + context 'when can_smart_action? is called' do + # test('canSmartAction() should return true when the permissions system is deactivate', function () { + # $post = [ + # 'data' => [ + # 'attributes' => [ + # 'values' => [], + # 'ids' => [1], + # 'collection_name' => 'Booking', + # 'parent_collection_name' => null, + # 'parent_collection_id' => null, + # 'parent_association_name' => null, + # 'all_records' => false, + # 'all_records_subset_query' => [ + # 'fields[Booking]' => 'id,title', + # 'page[number]' => 1, + # 'page[size]' => 15, + # 'sort' => '-id', + # 'timezone' => 'Europe/Paris', + # ], + # 'all_records_ids_excluded' => [], + # 'smart_action_id' => 'Booking-Mark@@@as@@@live', + # 'signed_approval_request' => null + # ], + # 'type' => 'custom-action-requests', + # ], + # ]; + # permissionsFactory($this, $post); + # $permissions = $this->bucket['permissions']; + # $collection = $this->bucket['collection']; + # Cache::put('forest.has_permission', false, config('permissionExpiration')); + # + # $request = Request::createFromGlobals(); + # $mockRequest = mock($request) + # ->makePartial() + # ->shouldAllowMockingProtectedMethods() + # ->shouldReceive('getPathInfo') + # ->andReturn('/forest/_actions/Booking/0/mark-as-live') + # ->shouldReceive('getMethod') + # ->andReturn('POST') + # ->getMock(); + # + # expect($permissions->canSmartAction($mockRequest, $collection, new Filter()))->toBeTrue(); + # }); + + # it 'should return true when the permissions system is deactivate' do + # args[:headers]['REQUEST_PATH'] = '/forest/_actions/Book/0/make-photocopy' + # args[:headers]['REQUEST_METHOD'] = 'POST' + # args[:params] = { + # data: { + # attributes: { + # 'values' => [], + # 'ids' => [1], + # 'collection_name' => 'Book', + # 'parent_collection_name' => nil, + # 'parent_collection_id' => nil, + # 'parent_association_name' => nil, + # 'all_records' => false, + # 'all_records_subset_query' => { + # 'fields[Book]' => 'id,title', + # 'page[number]' => 1, + # 'page[size]' => 15, + # 'sort' => '-id', + # 'timezone' => 'Europe/Paris', + # }, + # 'all_records_ids_excluded' => [], + # 'smart_action_id' => 'make-photocopy', + # 'signed_approval_request' => nil, + # }, + # 'type' => 'custom-action-requests', + # }, + # } + # + # @permissions.cache.get 'forest.has_permission' do + # 'fff' + # end + # puts @permissions.cache.inspect + # + # expect(@permissions.can_smart_action?(args, @datasource.collections['Book'], Filter.new)).to be true + # end + + it 'throws when the action is unknown' do + args[:headers]['REQUEST_PATH'] = '/forest/_actions/Book/0/fake-smart-action' + args[:headers]['REQUEST_METHOD'] = 'POST' + args[:params] = { + data: { + attributes: { + 'values' => [], + 'ids' => [1], + 'collection_name' => 'FakeCollection', + 'parent_collection_name' => nil, + 'parent_collection_id' => nil, + 'parent_association_name' => nil, + 'all_records' => false, + 'all_records_subset_query' => { + 'fields[Book]' => 'id,title', + 'page[number]' => 1, + 'page[size]' => 15, + 'sort' => '-id', + 'timezone' => 'Europe/Paris' + }, + 'all_records_ids_excluded' => [], + 'smart_action_id' => 'FakeCollection-fake-smart-action', + 'signed_approval_request' => nil + }, + 'type' => 'custom-action-requests' + } + } + + expect do + @permissions.can_smart_action?(args, @datasource.collections['Book'], + Filter.new) + end.to raise_error(ForestException, '🌳🌳🌳 The collection Book does not have this smart action') + end + + it "throws when the forest schema doesn't have any actions" do + args[:headers]['REQUEST_PATH'] = '/forest/_actions/FakeCollection/0/fake-smart-action' + args[:headers]['REQUEST_METHOD'] = 'POST' + args[:params] = { + data: { + attributes: { + 'values' => [], + 'ids' => [1], + 'collection_name' => 'FakeCollection', + 'parent_collection_name' => nil, + 'parent_collection_id' => nil, + 'parent_association_name' => nil, + 'all_records' => false, + 'all_records_subset_query' => { + 'fields[Book]' => 'id,title', + 'page[number]' => 1, + 'page[size]' => 15, + 'sort' => '-id', + 'timezone' => 'Europe/Paris' + }, + 'all_records_ids_excluded' => [], + 'smart_action_id' => 'FakeCollection-fake-smart-action', + 'signed_approval_request' => nil + }, + 'type' => 'custom-action-requests' + } + } + + expect do + @permissions.can_smart_action?(args, @datasource.collections['Book'], + Filter.new) + end.to raise_error(ForestException, '🌳🌳🌳 The collection Book does not have this smart action') + end + end + end + end +end From 678944badbb1751f15dcb42b174e6692a4a2f026 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 5 Dec 2023 15:06:46 +0100 Subject: [PATCH 30/49] chore: add package file-cache --- packages/forest_admin_agent/forest_admin_agent.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/forest_admin_agent/forest_admin_agent.gemspec b/packages/forest_admin_agent/forest_admin_agent.gemspec index 08018a515..0cb0ce50d 100644 --- a/packages/forest_admin_agent/forest_admin_agent.gemspec +++ b/packages/forest_admin_agent/forest_admin_agent.gemspec @@ -36,6 +36,7 @@ admin work on any Ruby application." spec.add_dependency "activesupport", ">= 6.1" spec.add_dependency "dry-container", "~> 0.11" spec.add_dependency "faraday", "~> 2.7" + spec.add_dependency "filecache", "~> 1.0" spec.add_dependency "ipaddress", "~> 0.8.3" spec.add_dependency "jsonapi-serializers", "~> 1.0" spec.add_dependency "jwt", "~> 2.7" From 05e592fcde75f41966600797752cddcf1cea2e73 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 5 Dec 2023 15:08:52 +0100 Subject: [PATCH 31/49] feat(permissions): replace cache with file-cache + update tests --- .../services/permissions.rb | 39 +++-- .../services/permissions_spec.rb | 138 ++++++------------ 2 files changed, 67 insertions(+), 110 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb index a091aff19..dc2005d5d 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb @@ -1,3 +1,5 @@ +require 'filecache' + module ForestAdminAgent module Services class Permissions @@ -11,19 +13,24 @@ class Permissions def initialize(caller) @caller = caller @forest_api = ForestAdminAgent::Http::ForestAdminApiRequester.new - @cache = Lightly.new( - life: Facades::Container.config_from_cache[:permission_expiration], - dir: "#{Facades::Container.config_from_cache[:cache_dir]}/permissions" + @cache = FileCache.new( + 'permissions', + Facades::Container.config_from_cache[:cache_dir].to_s, + Facades::Container.config_from_cache[:permission_expiration] ) end - def self.invalidate_cache(id_cache) - cache = Lightly.new( - life: Facades::Container.config_from_cache[:permission_expiration], - dir: "#{Facades::Container.config_from_cache[:cache_dir]}/permissions" + def self.invalidate_cache(id_cache = nil) + cache = FileCache.new( + 'permissions', + Facades::Container.config_from_cache[:cache_dir].to_s, + Facades::Container.config_from_cache[:permission_expiration] ) - cache.clear id_cache - # ... + + cache.clear if id_cache.nil? + + cache.delete(id_cache) unless cache.get(id_cache).nil? + # TODO: HANDLE LOGGER # logger.debug("Invalidating #{id_cache} cache..") end @@ -103,7 +110,7 @@ def get_scope(collection) end def get_user_data(user_id) - cache.get('forest.users') do + cache.get_or_set('forest.users') do response = fetch('/liana/v4/permissions/users') users = {} @@ -129,7 +136,7 @@ def get_team(rendering_id) def get_collections_permissions_data(force_fetch: false) self.class.invalidate_cache('forest.collections') if force_fetch == true - cache.get('forest.collections') do + cache.get_or_set('forest.collections') do response = fetch('/liana/v4/permissions/environment') collections = {} @@ -147,7 +154,7 @@ def get_collections_permissions_data(force_fetch: false) def get_chart_data(rendering_id, force_fetch: false) self.class.invalidate_cache('forest.stats') if force_fetch == true - cache.get('forest.stats') do + cache.get_or_set('forest.stats') do response = fetch("/liana/v4/permissions/renderings/#{rendering_id}") stat_hash = [] response[:stats].each do |stat| @@ -185,7 +192,7 @@ def array_hash(data) end def get_scope_and_team_data(rendering_id) - cache.get('forest.scopes') do + cache.get_or_set('forest.scopes') do data = {} response = fetch("/liana/v4/permissions/renderings/#{rendering_id}") @@ -197,10 +204,10 @@ def get_scope_and_team_data(rendering_id) end def permission_system? - cache.get('forest.has_permission') do + cache.get_or_set('forest.has_permission') do response = fetch('/liana/v4/permissions/environment') - !response.nil? - end + { enable: !response.nil? } + end[:enable] end def find_action_from_endpoint(collection_name, endpoint, http_method) diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/permissions_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/permissions_spec.rb index ba576269a..5b8264da1 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/permissions_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/permissions_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' require 'shared/caller' require 'faraday' +require 'filecache' module ForestAdminAgent module Services @@ -30,19 +31,13 @@ module Services let(:user_role_id) { 1 } before do - # clear cache + # clear main cache cache = Lightly.new( life: Facades::Container.config_from_cache[:permission_expiration], dir: Facades::Container.config_from_cache[:cache_dir].to_s ) cache.clear 'config' - # described_class::invalidate_cache('forest.users') - # described_class::invalidate_cache('forest.collections') - # described_class::invalidate_cache('forest.stats') - # described_class::invalidate_cache('forest.scopes') - # described_class::invalidate_cache('forest.has_permission') - @datasource = Datasource.new collection_book = instance_double( Collection, @@ -70,7 +65,8 @@ module Services schema_path: "#{__dir__}/../../../shared/.forestadmin-schema.json", forest_server_url: 'https://api.development.forestadmin.com', debug: true, - prefix: 'forest' + prefix: 'forest', + permission_expiration: 100 } ) @@ -197,21 +193,22 @@ module Services }.to_json) ) - # a - @permissions = described_class.new(QueryStringParser.parse_caller(args)) allow(@permissions).to receive(:forest_api).and_return(forest_api_requester) + + # clear permissions cache + described_class.invalidate_cache end context 'when invalidate_cache is called' do it 'invalidates the cache' do - @permissions.cache.get('foo') do + @permissions.cache.get_or_set('foo') do 'bar' end described_class.invalidate_cache('foo') - expect(@permissions.cache.cached?('has_permission')).to be false + expect(@permissions.cache.get('foo')).to be_nil end end @@ -549,84 +546,39 @@ module Services end context 'when can_smart_action? is called' do - # test('canSmartAction() should return true when the permissions system is deactivate', function () { - # $post = [ - # 'data' => [ - # 'attributes' => [ - # 'values' => [], - # 'ids' => [1], - # 'collection_name' => 'Booking', - # 'parent_collection_name' => null, - # 'parent_collection_id' => null, - # 'parent_association_name' => null, - # 'all_records' => false, - # 'all_records_subset_query' => [ - # 'fields[Booking]' => 'id,title', - # 'page[number]' => 1, - # 'page[size]' => 15, - # 'sort' => '-id', - # 'timezone' => 'Europe/Paris', - # ], - # 'all_records_ids_excluded' => [], - # 'smart_action_id' => 'Booking-Mark@@@as@@@live', - # 'signed_approval_request' => null - # ], - # 'type' => 'custom-action-requests', - # ], - # ]; - # permissionsFactory($this, $post); - # $permissions = $this->bucket['permissions']; - # $collection = $this->bucket['collection']; - # Cache::put('forest.has_permission', false, config('permissionExpiration')); - # - # $request = Request::createFromGlobals(); - # $mockRequest = mock($request) - # ->makePartial() - # ->shouldAllowMockingProtectedMethods() - # ->shouldReceive('getPathInfo') - # ->andReturn('/forest/_actions/Booking/0/mark-as-live') - # ->shouldReceive('getMethod') - # ->andReturn('POST') - # ->getMock(); - # - # expect($permissions->canSmartAction($mockRequest, $collection, new Filter()))->toBeTrue(); - # }); - - # it 'should return true when the permissions system is deactivate' do - # args[:headers]['REQUEST_PATH'] = '/forest/_actions/Book/0/make-photocopy' - # args[:headers]['REQUEST_METHOD'] = 'POST' - # args[:params] = { - # data: { - # attributes: { - # 'values' => [], - # 'ids' => [1], - # 'collection_name' => 'Book', - # 'parent_collection_name' => nil, - # 'parent_collection_id' => nil, - # 'parent_association_name' => nil, - # 'all_records' => false, - # 'all_records_subset_query' => { - # 'fields[Book]' => 'id,title', - # 'page[number]' => 1, - # 'page[size]' => 15, - # 'sort' => '-id', - # 'timezone' => 'Europe/Paris', - # }, - # 'all_records_ids_excluded' => [], - # 'smart_action_id' => 'make-photocopy', - # 'signed_approval_request' => nil, - # }, - # 'type' => 'custom-action-requests', - # }, - # } - # - # @permissions.cache.get 'forest.has_permission' do - # 'fff' - # end - # puts @permissions.cache.inspect - # - # expect(@permissions.can_smart_action?(args, @datasource.collections['Book'], Filter.new)).to be true - # end + it 'returns true when the permissions system is deactivate' do + args[:headers]['REQUEST_PATH'] = '/forest/_actions/Book/0/make-photocopy' + args[:headers]['REQUEST_METHOD'] = 'POST' + args[:params] = { + data: { + attributes: { + 'values' => [], + 'ids' => [1], + 'collection_name' => 'Book', + 'parent_collection_name' => nil, + 'parent_collection_id' => nil, + 'parent_association_name' => nil, + 'all_records' => false, + 'all_records_subset_query' => { + 'fields[Book]' => 'id,title', + 'page[number]' => 1, + 'page[size]' => 15, + 'sort' => '-id', + 'timezone' => 'Europe/Paris' + }, + 'all_records_ids_excluded' => [], + 'smart_action_id' => 'make-photocopy', + 'signed_approval_request' => nil + }, + 'type' => 'custom-action-requests' + } + } + + @permissions.cache.set('forest.has_permission', { enable: false }) + # puts @permissions.cache.get('forest.has_permission').inspect + + expect(@permissions.can_smart_action?(args, @datasource.collections['Book'], Filter.new)).to be true + end it 'throws when the action is unknown' do args[:headers]['REQUEST_PATH'] = '/forest/_actions/Book/0/fake-smart-action' @@ -657,8 +609,7 @@ module Services } expect do - @permissions.can_smart_action?(args, @datasource.collections['Book'], - Filter.new) + @permissions.can_smart_action?(args, @datasource.collections['Book'], Filter.new) end.to raise_error(ForestException, '🌳🌳🌳 The collection Book does not have this smart action') end @@ -691,8 +642,7 @@ module Services } expect do - @permissions.can_smart_action?(args, @datasource.collections['Book'], - Filter.new) + @permissions.can_smart_action?(args, @datasource.collections['Book'], Filter.new) end.to raise_error(ForestException, '🌳🌳🌳 The collection Book does not have this smart action') end end From 12d730bf65fa5d655702006215de5f15bca4d17f Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 6 Dec 2023 10:28:18 +0100 Subject: [PATCH 32/49] chore: add package ld-eventsource --- packages/forest_admin_agent/forest_admin_agent.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/forest_admin_agent/forest_admin_agent.gemspec b/packages/forest_admin_agent/forest_admin_agent.gemspec index 0cb0ce50d..52cf16129 100644 --- a/packages/forest_admin_agent/forest_admin_agent.gemspec +++ b/packages/forest_admin_agent/forest_admin_agent.gemspec @@ -40,6 +40,7 @@ admin work on any Ruby application." spec.add_dependency "ipaddress", "~> 0.8.3" spec.add_dependency "jsonapi-serializers", "~> 1.0" spec.add_dependency "jwt", "~> 2.7" + spec.add_dependency "ld-eventsource", "~> 2.2" spec.add_dependency "lightly", "~> 0.4.0" spec.add_dependency "mono_logger", "~> 1.1" spec.add_dependency "openid_connect", "~> 2.2" From a0b749c71169a8a48c302cddd26935e9da5e12e1 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 6 Dec 2023 10:33:08 +0100 Subject: [PATCH 33/49] chore: add new settings agent config instant_cache_refresh for SSE --- packages/forest_admin_rails/lib/forest_admin_rails.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/forest_admin_rails/lib/forest_admin_rails.rb b/packages/forest_admin_rails/lib/forest_admin_rails.rb index 59f5e1ee0..9280eb5f7 100644 --- a/packages/forest_admin_rails/lib/forest_admin_rails.rb +++ b/packages/forest_admin_rails/lib/forest_admin_rails.rb @@ -24,6 +24,7 @@ module ForestAdminRails setting :project_dir, default: Dir.pwd setting :loggerLevel, default: 'info' setting :logger, default: nil + setting :instant_cache_refresh, default: true # Rails.env.production? if defined?(Rails::Railtie) # logic for cors middleware,... here // or it might be into Engine From 310d2304774aa52e544523410528e70e866bf6ca Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 6 Dec 2023 11:15:28 +0100 Subject: [PATCH 34/49] feat(permission): add new service sse_cache_invalidation --- .../lib/forest_admin_agent.rb | 1 + .../services/sse_cache_invalidation.rb | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 packages/forest_admin_agent/lib/forest_admin_agent/services/sse_cache_invalidation.rb diff --git a/packages/forest_admin_agent/lib/forest_admin_agent.rb b/packages/forest_admin_agent/lib/forest_admin_agent.rb index 40ca59727..160119525 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent.rb @@ -3,6 +3,7 @@ loader = Zeitwerk::Loader.for_gem loader.inflector.inflect('oauth2' => 'OAuth2') +loader.inflector.inflect('sse_cache_invalidation' => 'SSECacheInvalidation') loader.setup module ForestAdminAgent diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/services/sse_cache_invalidation.rb b/packages/forest_admin_agent/lib/forest_admin_agent/services/sse_cache_invalidation.rb new file mode 100644 index 000000000..40d8bba00 --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/services/sse_cache_invalidation.rb @@ -0,0 +1,49 @@ +require 'ld-eventsource' + +module ForestAdminAgent + module Services + class SSECacheInvalidation + include ForestAdminDatasourceToolkit::Exceptions + + MESSAGE_CACHE_KEYS = { + 'refresh-users': %w[forest.users], + 'refresh-roles': %w[forest.collections], + 'refresh-renderings': %w[forest.collections forest.stats forest.scopes] + # TODO: add one for ip whitelist when server implement it + }.freeze + + def initialize + @permission_service = Permissions + end + + def run + uri = "#{Facades::Container.config_from_cache[:forest_server_url]}/liana/v4/subscribe-to-events" + headers = { + 'forest-secret-key' => Facades::Container.config_from_cache[:env_secret], + 'Accept' => 'text/event-stream' + } + + begin + SSE::Client.new(uri, headers: headers) do |client| + client.on_event do |event| + next if event.type == 'heartbeat' + + MESSAGE_CACHE_KEYS[event.type.to_sym]&.each do |cache_key| + @permission_service.invalidate_cache(cache_key) + # TODO: HANDLE LOGGER + # "info","invalidate cache {MESSAGE_CACHE_KEYS[event.type]} for event {event.type}" + end + # TODO: HANDLE LOGGER add else + # "info", "SSECacheInvalidation: unhandled message from server: {event.type}" + end + end + rescue StandardError + raise ForestException, 'Failed to reach SSE data from ForestAdmin server.' + # TODO: HANDLE LOGGER + # "debug", "SSE connection to forestadmin server due to ..." + # "warning", "SSE connection to forestadmin server closed unexpectedly, retrying." + end + end + end + end +end From 5c3c0f8676b1e085a95635cf5830b9a9c2a2fe9e Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 6 Dec 2023 11:16:58 +0100 Subject: [PATCH 35/49] chore: add call of sse_cache_invalidation into rails engine --- packages/forest_admin_rails/lib/forest_admin_rails/engine.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/forest_admin_rails/lib/forest_admin_rails/engine.rb b/packages/forest_admin_rails/lib/forest_admin_rails/engine.rb index ee001a105..757ea64c4 100644 --- a/packages/forest_admin_rails/lib/forest_admin_rails/engine.rb +++ b/packages/forest_admin_rails/lib/forest_admin_rails/engine.rb @@ -38,6 +38,9 @@ def load_configuration # setup agent require Rails.root.join('lib', 'forest_admin_rails', 'create_agent.rb') ForestAdminRails::CreateAgent.setup! + + sse = ForestAdminAgent::Services::SSECacheInvalidation + sse.new.run if ForestAdminRails.config[:instant_cache_refresh] end def load_cors From 7a963e0ac6fb9c438dfee37720fdf0daecb8466f Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 6 Dec 2023 15:08:45 +0100 Subject: [PATCH 36/49] chore: remove useless comment on permisions_spec --- .../spec/lib/forest_admin_agent/services/permissions_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/permissions_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/permissions_spec.rb index 5b8264da1..cdfa0386a 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/permissions_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/permissions_spec.rb @@ -575,7 +575,6 @@ module Services } @permissions.cache.set('forest.has_permission', { enable: false }) - # puts @permissions.cache.get('forest.has_permission').inspect expect(@permissions.can_smart_action?(args, @datasource.collections['Book'], Filter.new)).to be true end From ac145f69a405bc54abef3e72e283bdce24436392 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 6 Dec 2023 15:09:04 +0100 Subject: [PATCH 37/49] refactor: sse_cache_invalidation --- .../lib/forest_admin_agent/services/sse_cache_invalidation.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/services/sse_cache_invalidation.rb b/packages/forest_admin_agent/lib/forest_admin_agent/services/sse_cache_invalidation.rb index 40d8bba00..b6aec7e36 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/services/sse_cache_invalidation.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/services/sse_cache_invalidation.rb @@ -26,9 +26,9 @@ def run begin SSE::Client.new(uri, headers: headers) do |client| client.on_event do |event| - next if event.type == 'heartbeat' + next if event.type == :heartbeat - MESSAGE_CACHE_KEYS[event.type.to_sym]&.each do |cache_key| + MESSAGE_CACHE_KEYS[event.type]&.each do |cache_key| @permission_service.invalidate_cache(cache_key) # TODO: HANDLE LOGGER # "info","invalidate cache {MESSAGE_CACHE_KEYS[event.type]} for event {event.type}" From 383ef71c6a9a67ae9b7da81611aab1c4472037dd Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 6 Dec 2023 16:25:12 +0100 Subject: [PATCH 38/49] chore: add test on sse_cache_invalidation --- .../services/sse_cache_invalidation_spec.rb | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 packages/forest_admin_agent/spec/lib/forest_admin_agent/services/sse_cache_invalidation_spec.rb diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/sse_cache_invalidation_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/sse_cache_invalidation_spec.rb new file mode 100644 index 000000000..01fa2d9ef --- /dev/null +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/sse_cache_invalidation_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' +require 'ld-eventsource' + +module ForestAdminAgent + module Services + describe SSECacheInvalidation do + let(:sse_client) { instance_double(SSE::Client) } + + let(:permissions) do + class_double( + Permissions + ).as_stubbed_const + end + + let(:sse_heartbeat) { SSE::StreamEvent.new(:heartbeat, 'heartbeat', nil) } + + let(:see_foo) { SSE::StreamEvent.new(:foo, 'foo', nil) } + + let(:sse_refresh_users) { SSE::StreamEvent.new(:'refresh-users', 'refresh-users', nil) } + + let(:sse_refresh_roles) { SSE::StreamEvent.new(:'refresh-roles', 'refresh-roles', nil) } + + let(:sse_refresh_renderings) { SSE::StreamEvent.new(:'refresh-renderings', '{"renderingIds":[47]}', nil) } + + before do + allow(permissions).to receive(:invalidate_cache).with(any_args).and_return(nil) + end + + it 'does nothing when it receives heartbeat event' do + allow(SSE::Client).to receive(:new).and_yield(sse_client) + allow(sse_client).to receive(:on_event).and_yield(sse_heartbeat) + + expect(permissions).not_to have_received(:invalidate_cache) + described_class.new.run + end + + it 'does nothing when it receives unknown sse event' do + allow(SSE::Client).to receive(:new).and_yield(sse_client) + allow(sse_client).to receive(:on_event).and_yield(see_foo) + + expect(permissions).not_to have_received(:invalidate_cache) + described_class.new.run + end + + it 'invalidates cache forest.users when it receives refresh-users event' do + allow(SSE::Client).to receive(:new).and_yield(sse_client) + allow(sse_client).to receive(:on_event).and_yield(sse_refresh_users) + + described_class.new.run + expect(permissions).to have_received(:invalidate_cache).with('forest.users') + end + + it 'invalidates cache forest.collections when it receives refresh-roles event' do + allow(SSE::Client).to receive(:new).and_yield(sse_client) + allow(sse_client).to receive(:on_event).and_yield(sse_refresh_roles) + + described_class.new.run + expect(permissions).to have_received(:invalidate_cache).with('forest.collections') + end + + it 'invalidates cache forest.collections when it receives refresh-renderings event' do + allow(SSE::Client).to receive(:new).and_yield(sse_client) + allow(sse_client).to receive(:on_event).and_yield(sse_refresh_renderings) + + described_class.new.run + expect(permissions).to have_received(:invalidate_cache).with('forest.collections') + expect(permissions).to have_received(:invalidate_cache).with('forest.stats') + expect(permissions).to have_received(:invalidate_cache).with('forest.scopes') + end + end + end +end From 739008f8f8de32ba8b01c09833f9c97da830deec Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 7 Dec 2023 12:07:11 +0100 Subject: [PATCH 39/49] feat(permissions): add permission on routes --- .rubocop.yml | 1 + .../routes/resources/count.rb | 6 ++++- .../routes/resources/delete.rb | 6 ++++- .../routes/resources/list.rb | 9 ++++++- .../resources/related/associate_related.rb | 11 +++++++- .../routes/resources/related/count_related.rb | 3 ++- .../resources/related/dissociate_related.rb | 2 ++ .../routes/resources/related/list_related.rb | 10 +++++++- .../resources/related/update_related.rb | 9 ++++++- .../routes/resources/show.rb | 5 +++- .../routes/resources/store.rb | 1 + .../routes/resources/update.rb | 4 ++- .../routes/resources/count_spec.rb | 5 ++++ .../routes/resources/delete_spec.rb | 25 ++++++++++++++++--- .../routes/resources/list_spec.rb | 10 +++++++- .../related/associate_related_spec.rb | 12 ++++++++- .../resources/related/count_related_spec.rb | 6 ++++- .../related/dissociate_related_spec.rb | 10 ++++++++ .../resources/related/list_related_spec.rb | 14 +++++++++-- .../resources/related/update_related_spec.rb | 13 +++++++++- .../routes/resources/show_spec.rb | 14 ++++++++++- .../routes/resources/store_spec.rb | 6 +++++ .../routes/resources/update_spec.rb | 14 ++++++++++- 23 files changed, 176 insertions(+), 20 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 03994653b..169f72f45 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -226,6 +226,7 @@ Layout/LineLength: - 'packages/forest_admin_agent/spec/shared/caller.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/http/forest_admin_api_requester.rb' - 'packages/forest_admin_agent/spec/lib/forest_admin_agent/http/forest_admin_api_requester_spec.rb' + - 'packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb' - 'packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_injector_spec.rb' - 'packages/forest_admin_agent/spec/lib/forest_admin_agent/services/smart_action_checker_spec.rb' diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/count.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/count.rb index f2280a8a7..02d170157 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/count.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/count.rb @@ -5,6 +5,7 @@ module Routes module Resources class Count < AbstractAuthenticatedRoute include ForestAdminAgent::Builder + include ForestAdminDatasourceToolkit::Components::Query::ConditionTree def setup_routes add_route('forest_count', 'get', '/:collection_name/count', ->(args) { handle_request(args) }) @@ -13,9 +14,12 @@ def setup_routes def handle_request(args = {}) build(args) + @permissions.can?(:browse, @collection) if @collection.is_countable? - filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new + filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new( + condition_tree: ConditionTreeFactory.intersect([@permissions.get_scope(@collection)]) + ) aggregation = ForestAdminDatasourceToolkit::Components::Query::Aggregation.new(operation: 'Count') result = @collection.aggregate(@caller, filter, aggregation) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/delete.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/delete.rb index bd751d047..3419268ee 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/delete.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/delete.rb @@ -7,6 +7,7 @@ module Resources class Delete < AbstractAuthenticatedRoute include ForestAdminAgent::Builder include ForestAdminDatasourceToolkit::Components::Query + include ForestAdminDatasourceToolkit::Components::Query::ConditionTree def setup_routes add_route('forest_delete_bulk', 'delete', '/:collection_name', ->(args) { handle_request_bulk(args) }) @@ -17,6 +18,7 @@ def setup_routes def handle_request(args = {}) build(args) + @permissions.can?(:delete, @collection) id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true) delete_records(args, { ids: [id], are_excluded: false }) @@ -25,6 +27,7 @@ def handle_request(args = {}) def handle_request_bulk(args = {}) build(args) + @permissions.can?(:delete, @collection) selection_ids = Utils::Id.parse_selection_ids(@collection, args[:params], with_key: true) delete_records(args, selection_ids) @@ -38,7 +41,8 @@ def delete_records(args, selection_ids) condition_tree: ConditionTree::ConditionTreeFactory.intersect( [ Utils::QueryStringParser.parse_condition_tree(@collection, args), - condition_tree_ids + condition_tree_ids, + @permissions.get_scope(@collection) ] ) ) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb index 9bd964708..0204b4585 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb @@ -5,6 +5,7 @@ module Routes module Resources class List < AbstractAuthenticatedRoute include ForestAdminAgent::Builder + include ForestAdminDatasourceToolkit::Components::Query::ConditionTree def setup_routes add_route('forest_list', 'get', '/:collection_name', ->(args) { handle_request(args) }) @@ -13,9 +14,15 @@ def setup_routes def handle_request(args = {}) build(args) + @permissions.can?(:browse, @collection) filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new( - condition_tree: ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(@collection, args), + condition_tree: ConditionTreeFactory.intersect([ + @permissions.get_scope(@collection), + ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree( + @collection, args + ) + ]), page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args) ) projection = ForestAdminAgent::Utils::QueryStringParser.parse_projection_with_pks(@collection, args) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/associate_related.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/associate_related.rb index 950089cc3..8dc29186c 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/associate_related.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/associate_related.rb @@ -8,6 +8,7 @@ class AssociateRelated < AbstractRelatedRoute include ForestAdminAgent::Builder include ForestAdminDatasourceToolkit::Utils include ForestAdminDatasourceToolkit::Components::Query + def setup_routes add_route( 'forest_related_associate', @@ -21,6 +22,7 @@ def setup_routes def handle_request(args = {}) build(args) + @permissions.can?(:edit, @collection) parent_id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true) target_relation_id = Utils::Id.unpack_id(@child_collection, args[:params]['data'][0]['id'], with_key: true) @@ -40,7 +42,14 @@ def handle_request(args = {}) def associate_one_to_many(relation, parent_id, target_relation_id) id = Schema.primary_keys(@child_collection)[0] value = Collection.get_value(@child_collection, @caller, target_relation_id, id) - filter = Filter.new(condition_tree: ConditionTree::Nodes::ConditionTreeLeaf.new(id, 'Equal', value)) + filter = Filter.new( + condition_tree: ConditionTree::ConditionTreeFactory.intersect( + [ + ConditionTree::Nodes::ConditionTreeLeaf.new(id, 'Equal', value), + @permissions.get_scope(@collection) + ] + ) + ) value = Collection.get_value(@collection, @caller, parent_id, relation.origin_key_target) @child_collection.update(@caller, filter, { relation.origin_key => value }) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/count_related.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/count_related.rb index 6bf074817..c50f26bc7 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/count_related.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/count_related.rb @@ -21,9 +21,10 @@ def setup_routes def handle_request(args = {}) build(args) + @permissions.can?(:browse, @collection) if @child_collection.is_countable? - filter = Filter.new + filter = Filter.new(condition_tree: @permissions.get_scope(@collection)) id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true) result = Collection.aggregate_relation( @collection, diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/dissociate_related.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/dissociate_related.rb index 7835877cd..af113ba91 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/dissociate_related.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/dissociate_related.rb @@ -21,6 +21,7 @@ def setup_routes def handle_request(args = {}) build(args) + @permissions.can?(:delete, @collection) parent_id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true) is_delete_mode = !args.dig(:params, :delete).nil? @@ -84,6 +85,7 @@ def get_base_foreign_filter(args) Filter.new( condition_tree: ConditionTree::ConditionTreeFactory.intersect( [ + @permissions.get_scope(@child_collection), Utils::QueryStringParser.parse_condition_tree(@child_collection, args), selected_ids ] diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/list_related.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/list_related.rb index 08138556b..c7dffbed6 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/list_related.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/list_related.rb @@ -7,6 +7,8 @@ module Related class ListRelated < AbstractRelatedRoute include ForestAdminAgent::Builder include ForestAdminDatasourceToolkit::Utils + include ForestAdminDatasourceToolkit::Components::Query::ConditionTree + def setup_routes add_route( 'forest_related_list', @@ -20,10 +22,16 @@ def setup_routes def handle_request(args = {}) build(args) + @permissions.can?(:browse, @collection) # TODO: add csv behaviour filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new( - condition_tree: ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(@child_collection, args), + condition_tree: ConditionTreeFactory.intersect( + [ + @permissions.get_scope(@collection), + ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(@child_collection, args) + ] + ), page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args) ) projection = ForestAdminAgent::Utils::QueryStringParser.parse_projection_with_pks(@child_collection, args) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/update_related.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/update_related.rb index 8fabc44f7..d30a4360e 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/update_related.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/update_related.rb @@ -21,6 +21,7 @@ def setup_routes def handle_request(args = {}) build(args) + @permissions.can?(:edit, @collection) relation = @collection.fields[args[:params]['relation_name']] parent_id = Utils::Id.unpack_id(@collection, args[:params]['id']) @@ -61,6 +62,7 @@ def break_old_one_to_one_relationship(_scope, relation, origin_value, linked_id) old_fk_owner_filter = Filter.new( condition_tree: ConditionTree::ConditionTreeFactory.intersect( [ + @permissions.get_scope(@collection), ConditionTree::Nodes::ConditionTreeLeaf.new( relation.origin_key, ConditionTree::Operators::EQUAL, @@ -91,7 +93,12 @@ def create_new_one_to_one_relationship(_scope, relation, origin_value, linked_id @child_collection.update( @caller, - Filter.new(condition_tree: new_fk_owner), + Filter.new(condition_tree: ConditionTree::ConditionTreeFactory.intersect( + [ + @permissions.get_scope(@collection), + new_fk_owner + ] + )), { relation.origin_key => origin_value } ) end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb index d30310a64..3d91588cb 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb @@ -15,12 +15,15 @@ def setup_routes def handle_request(args = {}) build(args) + @permissions.can?(:read, @collection) + scope = @permissions.get_scope(@collection) id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true) condition_tree = ConditionTree::ConditionTreeFactory.match_records(@collection, [id]) filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new( - condition_tree: condition_tree, + condition_tree: ConditionTree::ConditionTreeFactory.intersect([condition_tree, scope]), page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args) ) + projection = ProjectionFactory.all(@collection) records = @collection.list(@caller, filter, projection) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb index ce88957c8..54a7aba50 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb @@ -15,6 +15,7 @@ def setup_routes def handle_request(args = {}) build(args) + @permissions.can?(:add, @collection) data = format_attributes(args) record = @collection.create(@caller, data) link_one_to_one_relations(args, record) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/update.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/update.rb index dff7b56d8..53c3f9e85 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/update.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/update.rb @@ -16,10 +16,12 @@ def setup_routes def handle_request(args = {}) build(args) + @permissions.can?(:edit, @collection) + scope = @permissions.get_scope(@collection) id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true) condition_tree = ConditionTree::ConditionTreeFactory.match_records(@collection, [id]) filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new( - condition_tree: condition_tree, + condition_tree: ConditionTree::ConditionTreeFactory.intersect([condition_tree, scope]), page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args) ) data = format_attributes(args) diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/count_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/count_spec.rb index 6c5384476..c6cdbcc4d 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/count_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/count_spec.rb @@ -8,6 +8,7 @@ module Routes module Resources include ForestAdminDatasourceToolkit include ForestAdminDatasourceToolkit::Schema + include ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes describe Count do include_context 'with caller' subject(:count) { described_class.new } @@ -20,6 +21,7 @@ module Resources } } end + let(:permissions) { instance_double(ForestAdminAgent::Services::Permissions) } before do @datasource = Datasource.new @@ -34,6 +36,9 @@ module Resources @datasource.add_collection(collection) ForestAdminAgent::Builder::AgentFactory.instance.add_datasource(@datasource) ForestAdminAgent::Builder::AgentFactory.instance.build + + allow(ForestAdminAgent::Services::Permissions).to receive(:new).and_return(permissions) + allow(permissions).to receive_messages(can?: true, get_scope: ConditionTreeBranch.new('Or', [])) end it 'adds the route forest_count' do diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/delete_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/delete_spec.rb index 4e3dcab84..044ebb549 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/delete_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/delete_spec.rb @@ -18,6 +18,12 @@ module Resources params: params } end + let(:permissions) { instance_double(ForestAdminAgent::Services::Permissions) } + + before do + allow(ForestAdminAgent::Services::Permissions).to receive(:new).and_return(permissions) + allow(permissions).to receive_messages(can?: true, get_scope: Nodes::ConditionTreeBranch.new('Or', [])) + end it 'adds the route forest_store' do delete.setup_routes @@ -83,7 +89,11 @@ def respond_to?(arg) expect(@datasource.collection('user')).to have_received(:delete) do |caller, filter| expect(caller).to be_instance_of(Components::Caller) - expect(filter.condition_tree.to_h).to eq({ field: 'id', operator: Operators::EQUAL, value: 1 }) + expect(filter.condition_tree.to_h).to eq(aggregator: 'And', + conditions: [ + { field: 'id', operator: Operators::EQUAL, value: 1 }, + { aggregator: 'Or', conditions: [] } + ]) end end end @@ -126,7 +136,11 @@ def respond_to?(arg) expect(@datasource.collection('user')).to have_received(:delete) do |caller, filter| expect(caller).to be_instance_of(Components::Caller) - expect(filter.condition_tree.to_h).to eq({ field: 'id', operator: Operators::IN, value: [1, 2, 3] }) + expect(filter.condition_tree.to_h).to eq(aggregator: 'And', + conditions: [ + { field: 'id', operator: Operators::IN, value: [1, 2, 3] }, + { aggregator: 'Or', conditions: [] } + ]) end end @@ -137,7 +151,12 @@ def respond_to?(arg) expect(@datasource.collection('user')).to have_received(:delete) do |caller, filter| expect(caller).to be_instance_of(Components::Caller) - expect(filter.condition_tree.to_h).to eq({ field: 'id', operator: Operators::NOT_IN, value: [1, 2, 3] }) + expect(filter.condition_tree.to_h).to eq(aggregator: 'And', + conditions: [ + { field: 'id', operator: Operators::NOT_IN, + value: [1, 2, 3] }, + { aggregator: 'Or', conditions: [] } + ]) end end end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/list_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/list_spec.rb index bbce3827a..fbe82b430 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/list_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/list_spec.rb @@ -22,6 +22,7 @@ module Resources } } end + let(:permissions) { instance_double(ForestAdminAgent::Services::Permissions) } before do user_class = Struct.new(:id, :first_name, :last_name) @@ -42,6 +43,9 @@ module Resources @datasource.add_collection(collection) ForestAdminAgent::Builder::AgentFactory.instance.add_datasource(@datasource) ForestAdminAgent::Builder::AgentFactory.instance.build + + allow(ForestAdminAgent::Services::Permissions).to receive(:new).and_return(permissions) + allow(permissions).to receive_messages(can?: true, get_scope: Nodes::ConditionTreeBranch.new('Or', [])) end it 'adds the route forest_list' do @@ -78,7 +82,10 @@ module Resources expect(@datasource.collection('user')).to have_received(:list) do |caller, filter, projection| expect(caller).to be_instance_of(Components::Caller) - expect(filter.condition_tree.to_h).to eq({ field: 'id', operator: Operators::GREATER_THAN, value: 7 }) + expect(filter.condition_tree.to_h).to eq(aggregator: 'And', + conditions: [{ aggregator: 'Or', conditions: [] }, + { field: 'id', operator: Operators::GREATER_THAN, + value: 7 }]) expect(projection).to eq(%w[id first_name last_name]) end end @@ -103,6 +110,7 @@ module Resources { aggregator: 'And', conditions: [ + { aggregator: 'Or', conditions: [] }, { field: 'id', operator: 'Greater_Than', value: 7 }, { field: 'first_name', operator: 'Contains', value: 'foo' } ] diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/associate_related_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/associate_related_spec.rb index 7918b6b3f..3bfc8f469 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/associate_related_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/associate_related_spec.rb @@ -23,6 +23,7 @@ module Related } } end + let(:permissions) { instance_double(ForestAdminAgent::Services::Permissions) } before do user_class = Struct.new(:id, :name) @@ -86,6 +87,9 @@ module Related @datasource.add_collection(collection_address) ForestAdminAgent::Builder::AgentFactory.instance.add_datasource(@datasource) ForestAdminAgent::Builder::AgentFactory.instance.build + + allow(ForestAdminAgent::Services::Permissions).to receive(:new).and_return(permissions) + allow(permissions).to receive_messages(can?: true, get_scope: Nodes::ConditionTreeBranch.new('Or', [])) end it 'adds the route forest_related_associate' do @@ -106,7 +110,13 @@ module Related expect(@datasource.collection('address_user')).to have_received(:update) do |caller, filter, data| expect(caller).to be_instance_of(Components::Caller) expect(filter).to have_attributes( - condition_tree: have_attributes(field: 'id', operator: Operators::EQUAL, value: 1), + condition_tree: have_attributes( + aggregator: 'And', + conditions: [ + have_attributes(field: 'id', operator: Operators::EQUAL, value: 1), + have_attributes(aggregator: 'Or', conditions: []) + ] + ), page: nil, search: nil, search_extended: nil, diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/count_related_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/count_related_spec.rb index 8848cd935..1b4cc9940 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/count_related_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/count_related_spec.rb @@ -23,6 +23,7 @@ module Related } } end + let(:permissions) { instance_double(ForestAdminAgent::Services::Permissions) } before do user_class = Struct.new(:id, :first_name, :last_name) @@ -57,6 +58,9 @@ module Related @datasource.add_collection(collection_category) ForestAdminAgent::Builder::AgentFactory.instance.add_datasource(@datasource) ForestAdminAgent::Builder::AgentFactory.instance.build + + allow(ForestAdminAgent::Services::Permissions).to receive(:new).and_return(permissions) + allow(permissions).to receive_messages(can?: true, get_scope: Nodes::ConditionTreeBranch.new('Or', [])) end it 'adds the route forest_related_count' do @@ -80,7 +84,7 @@ module Related expect(id).to eq({ 'id' => 1 }) expect(relation_name).to eq('category') expect(foreign_filter).to have_attributes( - condition_tree: nil, + condition_tree: have_attributes(aggregator: 'Or', conditions: []), page: nil, search: nil, search_extended: nil, diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/dissociate_related_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/dissociate_related_spec.rb index 1099894d7..3f9f6ddd8 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/dissociate_related_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/dissociate_related_spec.rb @@ -13,6 +13,7 @@ module Related subject(:dissociate) { described_class.new } let(:datasource) { Datasource.new } + let(:permissions) { instance_double(ForestAdminAgent::Services::Permissions) } before do collection_user = Collection.new(datasource, 'user') @@ -70,6 +71,9 @@ module Related datasource.add_collection(collection_address) ForestAdminAgent::Builder::AgentFactory.instance.add_datasource(datasource) ForestAdminAgent::Builder::AgentFactory.instance.build + + allow(ForestAdminAgent::Services::Permissions).to receive(:new).and_return(permissions) + allow(permissions).to receive_messages(can?: true, get_scope: Nodes::ConditionTreeBranch.new('Or', [])) end it 'adds the route forest_related_dissociate' do @@ -113,6 +117,7 @@ module Related condition_tree: have_attributes( aggregator: 'And', conditions: [ + have_attributes(aggregator: 'Or', conditions: []), have_attributes(field: 'id', operator: Operators::EQUAL, value: 1), have_attributes(field: 'user_id', operator: Operators::EQUAL, value: 1) ] @@ -146,6 +151,7 @@ module Related condition_tree: have_attributes( aggregator: 'And', conditions: [ + have_attributes(aggregator: 'Or', conditions: []), have_attributes(field: 'id', operator: Operators::EQUAL, value: 1), have_attributes(field: 'user_id', operator: Operators::EQUAL, value: 1) ] @@ -183,6 +189,7 @@ module Related condition_tree: have_attributes( aggregator: 'And', conditions: [ + have_attributes(aggregator: 'Or', conditions: []), have_attributes(field: 'id', operator: Operators::NOT_EQUAL, value: 2), have_attributes(field: 'user_id', operator: Operators::EQUAL, value: 1) ] @@ -229,6 +236,7 @@ module Related aggregator: 'And', conditions: [ have_attributes(field: 'user_id', operator: Operators::EQUAL, value: 1), + have_attributes(aggregator: 'Or', conditions: []), have_attributes(field: 'address_id:id', operator: Operators::EQUAL, value: 1) ] ), @@ -262,6 +270,7 @@ module Related aggregator: 'And', conditions: [ have_attributes(field: 'user_id', operator: Operators::EQUAL, value: 1), + have_attributes(aggregator: 'Or', conditions: []), have_attributes(field: 'address_id:id', operator: Operators::EQUAL, value: 1) ] ), @@ -279,6 +288,7 @@ module Related condition_tree: have_attributes( aggregator: 'And', conditions: [ + have_attributes(aggregator: 'Or', conditions: []), have_attributes(field: 'id', operator: Operators::EQUAL, value: 1), have_attributes(field: 'id', operator: Operators::IN, value: [1]) ] diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/list_related_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/list_related_spec.rb index 8093e0825..77e6d4447 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/list_related_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/list_related_spec.rb @@ -23,6 +23,7 @@ module Related } } end + let(:permissions) { instance_double(ForestAdminAgent::Services::Permissions) } before do user_class = Struct.new(:id, :first_name, :last_name) @@ -57,6 +58,9 @@ module Related @datasource.add_collection(collection_category) ForestAdminAgent::Builder::AgentFactory.instance.add_datasource(@datasource) ForestAdminAgent::Builder::AgentFactory.instance.build + + allow(ForestAdminAgent::Services::Permissions).to receive(:new).and_return(permissions) + allow(permissions).to receive_messages(can?: true, get_scope: Nodes::ConditionTreeBranch.new('Or', [])) end it 'adds the route forest_related_list' do @@ -79,7 +83,7 @@ module Related expect(id).to eq({ 'id' => 1 }) expect(relation_name).to eq('category') expect(foreign_filter).to have_attributes( - condition_tree: nil, + condition_tree: have_attributes(aggregator: 'Or', conditions: []), page: be_instance_of(ForestAdminDatasourceToolkit::Components::Query::Page), search: nil, search_extended: nil, @@ -106,7 +110,13 @@ module Related expect(id).to eq({ 'id' => 1 }) expect(relation_name).to eq('category') expect(foreign_filter).to have_attributes( - condition_tree: have_attributes(field: 'id', operator: Operators::GREATER_THAN, value: 7), + condition_tree: have_attributes( + aggregator: 'And', + conditions: [ + have_attributes(aggregator: 'Or', conditions: []), + have_attributes(field: 'id', operator: Operators::GREATER_THAN, value: 7) + ] + ), page: be_instance_of(ForestAdminDatasourceToolkit::Components::Query::Page), search: nil, search_extended: nil, diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/update_related_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/update_related_spec.rb index a2594c252..a4d1ea060 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/update_related_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/update_related_spec.rb @@ -13,6 +13,7 @@ module Related subject(:update) { described_class.new } let(:datasource) { Datasource.new } + let(:permissions) { instance_double(ForestAdminAgent::Services::Permissions) } before do collection_user = Collection.new(datasource, 'user') @@ -47,6 +48,9 @@ module Related datasource.add_collection(collection_book) ForestAdminAgent::Builder::AgentFactory.instance.add_datasource(datasource) ForestAdminAgent::Builder::AgentFactory.instance.build + + allow(ForestAdminAgent::Services::Permissions).to receive(:new).and_return(permissions) + allow(permissions).to receive_messages(can?: true, get_scope: Nodes::ConditionTreeBranch.new('Or', [])) end it 'adds the route forest_related_update' do @@ -114,6 +118,7 @@ module Related condition_tree: have_attributes( aggregator: 'And', conditions: [ + have_attributes(aggregator: 'Or', conditions: []), have_attributes(field: 'author_id', operator: Operators::EQUAL, value: 1), have_attributes(field: 'id', operator: Operators::NOT_EQUAL, value: 1) ] @@ -129,7 +134,13 @@ module Related [ Components::Caller, { - condition_tree: have_attributes(field: 'id', operator: Operators::EQUAL, value: 1), + condition_tree: have_attributes( + aggregator: 'And', + conditions: [ + have_attributes(aggregator: 'Or', conditions: []), + have_attributes(field: 'id', operator: Operators::EQUAL, value: 1) + ] + ), page: nil, search: nil, search_extended: nil, diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/show_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/show_spec.rb index f00a02d33..58d45ba40 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/show_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/show_spec.rb @@ -21,6 +21,12 @@ module Resources } } end + let(:permissions) { instance_double(ForestAdminAgent::Services::Permissions) } + + before do + allow(ForestAdminAgent::Services::Permissions).to receive(:new).and_return(permissions) + allow(permissions).to receive_messages(can?: true, get_scope: Nodes::ConditionTreeBranch.new('Or', [])) + end it 'adds the route forest_list' do show.setup_routes @@ -92,7 +98,13 @@ def respond_to?(arg) expect(@datasource.collection('user')).to have_received(:list) do |caller, filter, projection| expect(caller).to be_instance_of(Components::Caller) - expect(filter.condition_tree.to_h).to eq({ field: 'id', operator: Operators::EQUAL, value: 1 }) + expect(filter.condition_tree.to_h).to eq({ + aggregator: 'And', + conditions: [ + { field: 'id', operator: Operators::EQUAL, value: 1 }, + { aggregator: 'Or', conditions: [] } + ] + }) expect(projection).to eq(%w[id first_name last_name]) end end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/store_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/store_spec.rb index 8212edf23..6aee14201 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/store_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/store_spec.rb @@ -21,6 +21,12 @@ module Resources } } end + let(:permissions) { instance_double(ForestAdminAgent::Services::Permissions) } + + before do + allow(ForestAdminAgent::Services::Permissions).to receive(:new).and_return(permissions) + allow(permissions).to receive(:can?).and_return(true) + end it 'adds the route forest_store' do store.setup_routes diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/update_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/update_spec.rb index d4b219613..efc1da45c 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/update_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/update_spec.rb @@ -21,6 +21,12 @@ module Resources } } end + let(:permissions) { instance_double(ForestAdminAgent::Services::Permissions) } + + before do + allow(ForestAdminAgent::Services::Permissions).to receive(:new).and_return(permissions) + allow(permissions).to receive_messages(can?: true, get_scope: Nodes::ConditionTreeBranch.new('Or', [])) + end it 'adds the route forest_store' do update.setup_routes @@ -89,7 +95,13 @@ def respond_to?(arg) expect(@datasource.collection('book')).to have_received(:update) do |caller, filter, data| expect(caller).to be_instance_of(Components::Caller) expect(data).to eq({ 'title' => 'Harry potter and the goblet of fire' }) - expect(filter.condition_tree.to_h).to eq({ field: 'id', operator: Operators::EQUAL, value: 1 }) + expect(filter.condition_tree.to_h).to eq( + aggregator: 'And', + conditions: [ + { field: 'id', operator: Operators::EQUAL, value: 1 }, + { aggregator: 'Or', conditions: [] } + ] + ) end end end From b7be343bbd9273d1503dfe5ea667d5fa52cc3361 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 7 Dec 2023 14:03:42 +0100 Subject: [PATCH 40/49] refactor: set run method to method class for sse_invalidation_cache --- .../services/sse_cache_invalidation.rb | 8 ++------ .../services/sse_cache_invalidation_spec.rb | 16 ++++++---------- .../lib/forest_admin_rails/engine.rb | 2 +- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/services/sse_cache_invalidation.rb b/packages/forest_admin_agent/lib/forest_admin_agent/services/sse_cache_invalidation.rb index b6aec7e36..7137e2a03 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/services/sse_cache_invalidation.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/services/sse_cache_invalidation.rb @@ -12,11 +12,7 @@ class SSECacheInvalidation # TODO: add one for ip whitelist when server implement it }.freeze - def initialize - @permission_service = Permissions - end - - def run + def self.run uri = "#{Facades::Container.config_from_cache[:forest_server_url]}/liana/v4/subscribe-to-events" headers = { 'forest-secret-key' => Facades::Container.config_from_cache[:env_secret], @@ -29,7 +25,7 @@ def run next if event.type == :heartbeat MESSAGE_CACHE_KEYS[event.type]&.each do |cache_key| - @permission_service.invalidate_cache(cache_key) + Permissions.invalidate_cache(cache_key) # TODO: HANDLE LOGGER # "info","invalidate cache {MESSAGE_CACHE_KEYS[event.type]} for event {event.type}" end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/sse_cache_invalidation_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/sse_cache_invalidation_spec.rb index 01fa2d9ef..6dea7dcc6 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/sse_cache_invalidation_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/sse_cache_invalidation_spec.rb @@ -6,11 +6,7 @@ module Services describe SSECacheInvalidation do let(:sse_client) { instance_double(SSE::Client) } - let(:permissions) do - class_double( - Permissions - ).as_stubbed_const - end + let(:permissions) { class_double(Permissions).as_stubbed_const } let(:sse_heartbeat) { SSE::StreamEvent.new(:heartbeat, 'heartbeat', nil) } @@ -31,7 +27,7 @@ module Services allow(sse_client).to receive(:on_event).and_yield(sse_heartbeat) expect(permissions).not_to have_received(:invalidate_cache) - described_class.new.run + described_class.run end it 'does nothing when it receives unknown sse event' do @@ -39,14 +35,14 @@ module Services allow(sse_client).to receive(:on_event).and_yield(see_foo) expect(permissions).not_to have_received(:invalidate_cache) - described_class.new.run + described_class.run end it 'invalidates cache forest.users when it receives refresh-users event' do allow(SSE::Client).to receive(:new).and_yield(sse_client) allow(sse_client).to receive(:on_event).and_yield(sse_refresh_users) - described_class.new.run + described_class.run expect(permissions).to have_received(:invalidate_cache).with('forest.users') end @@ -54,7 +50,7 @@ module Services allow(SSE::Client).to receive(:new).and_yield(sse_client) allow(sse_client).to receive(:on_event).and_yield(sse_refresh_roles) - described_class.new.run + described_class.run expect(permissions).to have_received(:invalidate_cache).with('forest.collections') end @@ -62,7 +58,7 @@ module Services allow(SSE::Client).to receive(:new).and_yield(sse_client) allow(sse_client).to receive(:on_event).and_yield(sse_refresh_renderings) - described_class.new.run + described_class.run expect(permissions).to have_received(:invalidate_cache).with('forest.collections') expect(permissions).to have_received(:invalidate_cache).with('forest.stats') expect(permissions).to have_received(:invalidate_cache).with('forest.scopes') diff --git a/packages/forest_admin_rails/lib/forest_admin_rails/engine.rb b/packages/forest_admin_rails/lib/forest_admin_rails/engine.rb index 757ea64c4..e784f36f4 100644 --- a/packages/forest_admin_rails/lib/forest_admin_rails/engine.rb +++ b/packages/forest_admin_rails/lib/forest_admin_rails/engine.rb @@ -40,7 +40,7 @@ def load_configuration ForestAdminRails::CreateAgent.setup! sse = ForestAdminAgent::Services::SSECacheInvalidation - sse.new.run if ForestAdminRails.config[:instant_cache_refresh] + sse.run if ForestAdminRails.config[:instant_cache_refresh] end def load_cors From e55d48f97309165d0983806369adeb4c0c2e8a04 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 7 Dec 2023 14:24:27 +0100 Subject: [PATCH 41/49] chore: update rubocop + lint --- .rubocop.yml | 4 ++++ .../lib/forest_admin_agent/auth/oauth2/forest_provider.rb | 2 +- .../lib/forest_admin_agent/builder/agent_factory.rb | 2 +- .../http/Exceptions/authentication_open_id_client.rb | 2 +- .../forest_admin_agent/http/Exceptions/conflict_error.rb | 2 +- .../forest_admin_agent/http/Exceptions/forbidden_error.rb | 2 +- .../forest_admin_agent/http/Exceptions/not_found_error.rb | 2 +- .../forest_admin_agent/http/Exceptions/require_approval.rb | 2 +- .../lib/forest_admin_agent/routes/resources/delete_spec.rb | 2 +- .../lib/forest_admin_agent/routes/resources/show_spec.rb | 2 +- .../lib/forest_admin_agent/routes/resources/store_spec.rb | 6 +++--- .../lib/forest_admin_agent/routes/resources/update_spec.rb | 2 +- .../forest_admin_datasource_active_record/parser/column.rb | 4 ++-- .../forest_admin_datasource_active_record/utils/query.rb | 2 +- .../exceptions/forest_exception.rb | 2 +- 15 files changed, 21 insertions(+), 17 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 169f72f45..2982ac4cc 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -147,6 +147,10 @@ Style/RedundantConstantBase: Exclude: - 'packages/forest_admin_rails/spec/rails_helper.rb' +Style/HashEachMethods: + Exclude: + - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/condition_tree_factory.rb' + Lint/NestedMethodDefinition: Exclude: - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/filter_factory.rb' diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/forest_provider.rb b/packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/forest_provider.rb index 0488c4990..6f0bec64d 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/forest_provider.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/forest_provider.rb @@ -8,7 +8,7 @@ class ForestProvider < OpenIDConnect::Client attr_reader :rendering_id def initialize(rendering_id, attributes = {}) - super attributes + super(attributes) @rendering_id = rendering_id @authorization_endpoint = '/oidc/auth' @token_endpoint = '/oidc/token' diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb b/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb index ca4872990..9183d4338 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb @@ -21,7 +21,7 @@ def setup(options) end def add_datasource(datasource) - datasource.collections.each { |_name, collection| @customizer.add_collection(collection) } + datasource.collections.each_value { |collection| @customizer.add_collection(collection) } self end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/authentication_open_id_client.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/authentication_open_id_client.rb index 2cdeeeedf..8ca645056 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/authentication_open_id_client.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/authentication_open_id_client.rb @@ -5,7 +5,7 @@ class AuthenticationOpenIdClient < HttpException attr_reader :error, :error_description, :state def initialize(error, error_description, state) - super error, 401, error_description + super(error, 401, error_description) @error = error @error_description = error_description @state = state diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/conflict_error.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/conflict_error.rb index dbdb59f02..3893aa606 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/conflict_error.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/conflict_error.rb @@ -6,7 +6,7 @@ class ConflictError < HttpException def initialize(message, name = 'ConflictError') @name = name - super 429, 'Conflict', message + super(429, 'Conflict', message) end end end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/forbidden_error.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/forbidden_error.rb index d05ea1f0f..eaf88451b 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/forbidden_error.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/forbidden_error.rb @@ -6,7 +6,7 @@ class ForbiddenError < HttpException def initialize(message, name = 'ForbiddenError') @name = name - super 403, 'Forbidden', message + super(403, 'Forbidden', message) end end end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/not_found_error.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/not_found_error.rb index 7e1da5633..c194a4d06 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/not_found_error.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/not_found_error.rb @@ -5,7 +5,7 @@ class NotFoundError < StandardError attr_reader :name, :status def initialize(msg, name = 'NotFoundError') - super msg + super(msg) @name = name @status = 404 end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/require_approval.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/require_approval.rb index ebb4d43eb..29ed1f764 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/require_approval.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/require_approval.rb @@ -7,7 +7,7 @@ class RequireApproval < HttpException def initialize(message, name = 'RequireApproval', data = []) @name = name @data = data - super 403, 'Forbidden', message + super(403, 'Forbidden', message) end end end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/delete_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/delete_spec.rb index 044ebb549..e9cb33b2e 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/delete_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/delete_spec.rb @@ -38,7 +38,7 @@ module Resources def respond_to?(arg) return false if arg == :each - super arg + super(arg) end end stub_const('User', user_class) diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/show_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/show_spec.rb index 58d45ba40..18a1b91d4 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/show_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/show_spec.rb @@ -40,7 +40,7 @@ module Resources def respond_to?(arg) return false if arg == :each - super arg + super(arg) end end stub_const('User', user_class) diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/store_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/store_spec.rb index 6aee14201..5536aae9c 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/store_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/store_spec.rb @@ -40,7 +40,7 @@ module Resources def respond_to?(arg) return false if arg == :each - super arg + super(arg) end end stub_const('Book', book_class) @@ -106,7 +106,7 @@ def respond_to?(arg) def respond_to?(arg) return false if arg == :each - super arg + super(arg) end end @@ -114,7 +114,7 @@ def respond_to?(arg) def respond_to?(arg) return false if arg == :each - super arg + super(arg) end end stub_const('Person', person_class) diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/update_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/update_spec.rb index efc1da45c..e057cc6b7 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/update_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/update_spec.rb @@ -40,7 +40,7 @@ module Resources def respond_to?(arg) return false if arg == :each - super arg + super(arg) end end stub_const('Book', book_class) diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/column.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/column.rb index 1bf7fde4d..e578c6173 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/column.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/column.rb @@ -27,7 +27,7 @@ def get_column_type(model, column) return 'Enum' end - is_array = (column.respond_to?(:array) && column.array == true) + is_array = column.respond_to?(:array) && column.array == true is_array ? "[#{TYPES[column.type]}]" : TYPES[column.type] end @@ -37,7 +37,7 @@ def get_enum_values(model, column) if sti_column?(model, column) model.descendants.each { |sti_model| enum_values << sti_model.name } else - model.defined_enums[column.name].each { |name, _value| enum_values << name } + model.defined_enums[column.name].each_key { |name| enum_values << name } end end enum_values diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb index 734d7a91c..1a33dc740 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb @@ -74,7 +74,7 @@ def compute_main_operator(condition_tree, aggregator) def select unless @projection.nil? query_select = @projection.columns.map { |field| "#{@collection.model.table_name}.#{field}" } - @projection.relations.each do |relation, _fields| + @projection.relations.each_key do |relation| relation_schema = @collection.fields[relation] if relation_schema.type == 'OneToOne' query_select.push("#{@collection.model.table_name}.#{relation_schema.origin_key_target}") diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/exceptions/forest_exception.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/exceptions/forest_exception.rb index 861b2039e..406ef90d8 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/exceptions/forest_exception.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/exceptions/forest_exception.rb @@ -3,7 +3,7 @@ module Exceptions class ForestException < RuntimeError def initialize(msg = '') msg = "🌳🌳🌳 #{msg}" - super msg + super(msg) end end end From 4a63cf87048bbac91b8542e67266eaef48074a94 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 7 Dec 2023 14:41:20 +0100 Subject: [PATCH 42/49] fix(test): forest_admin_api_requester --- .../http/forest_admin_api_requester_spec.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/http/forest_admin_api_requester_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/http/forest_admin_api_requester_spec.rb index 82240b9e9..36f942364 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/http/forest_admin_api_requester_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/http/forest_admin_api_requester_spec.rb @@ -34,13 +34,13 @@ module Http context 'when the handle response method is called' do it 'raises an exception when the error is a ForestException' do expect do - described_class.handle_response_error(ForestAdminDatasourceToolkit::Exceptions::ForestException.new('test')) + forest_admin_api_requester.handle_response_error(ForestAdminDatasourceToolkit::Exceptions::ForestException.new('test')) end.to raise_error(ForestAdminDatasourceToolkit::Exceptions::ForestException) end it 'raises an exception when the error message contains certificate' do expect do - described_class.handle_response_error(Faraday::ConnectionFailed.new('test', { message: 'certificate' })) + forest_admin_api_requester.handle_response_error(Faraday::ConnectionFailed.new('test', { message: 'certificate' })) end.to raise_error( ForestAdminDatasourceToolkit::Exceptions::ForestException, '🌳🌳🌳 ForestAdmin server TLS certificate cannot be verified. Please check that your system time is set properly.' @@ -49,7 +49,7 @@ module Http it 'raises an exception when the error code is 0' do expect do - described_class.handle_response_error(Faraday::ConnectionFailed.new('test', { status: 0 })) + forest_admin_api_requester.handle_response_error(Faraday::ConnectionFailed.new('test', { status: 0 })) end.to raise_error( ForestAdminDatasourceToolkit::Exceptions::ForestException, '🌳🌳🌳 Failed to reach ForestAdmin server. Are you online?' @@ -58,7 +58,7 @@ module Http it 'raises an exception when the error code is 502' do expect do - described_class.handle_response_error(Faraday::ConnectionFailed.new('test', { status: 502 })) + forest_admin_api_requester.handle_response_error(Faraday::ConnectionFailed.new('test', { status: 502 })) end.to raise_error( ForestAdminDatasourceToolkit::Exceptions::ForestException, '🌳🌳🌳 Failed to reach ForestAdmin server. Are you online?' @@ -67,7 +67,7 @@ module Http it 'raises an exception when the error code is 404' do expect do - described_class.handle_response_error(Faraday::ConnectionFailed.new('test', { status: 404 })) + forest_admin_api_requester.handle_response_error(Faraday::ConnectionFailed.new('test', { status: 404 })) end.to raise_error( ForestAdminDatasourceToolkit::Exceptions::ForestException, '🌳🌳🌳 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?' @@ -76,7 +76,7 @@ module Http it 'raises an exception when the error code is 503' do expect do - described_class.handle_response_error(Faraday::ConnectionFailed.new('test', { status: 503 })) + forest_admin_api_requester.handle_response_error(Faraday::ConnectionFailed.new('test', { status: 503 })) end.to raise_error( ForestAdminDatasourceToolkit::Exceptions::ForestException, '🌳🌳🌳 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.' @@ -85,7 +85,7 @@ module Http it 'raises an exception when the error code is 500' do expect do - described_class.handle_response_error(Faraday::ConnectionFailed.new('test', { status: 500 })) + forest_admin_api_requester.handle_response_error(Faraday::ConnectionFailed.new('test', { status: 500 })) end.to raise_error( ForestAdminDatasourceToolkit::Exceptions::ForestException, '🌳🌳🌳 An unexpected error occurred while contacting the ForestAdmin server. Please contact support@forestadmin.com for further investigations.' From 242bcec9fb365fe0fc9c0ac62d4d7058bf8267ad Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 7 Dec 2023 14:42:46 +0100 Subject: [PATCH 43/49] fix: default value of instant_cache_refresh --- packages/forest_admin_rails/lib/forest_admin_rails.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/forest_admin_rails/lib/forest_admin_rails.rb b/packages/forest_admin_rails/lib/forest_admin_rails.rb index 9280eb5f7..3e4f9b339 100644 --- a/packages/forest_admin_rails/lib/forest_admin_rails.rb +++ b/packages/forest_admin_rails/lib/forest_admin_rails.rb @@ -24,7 +24,7 @@ module ForestAdminRails setting :project_dir, default: Dir.pwd setting :loggerLevel, default: 'info' setting :logger, default: nil - setting :instant_cache_refresh, default: true # Rails.env.production? + setting :instant_cache_refresh, default: Rails.env.production? if defined?(Rails::Railtie) # logic for cors middleware,... here // or it might be into Engine From 5f60d9f2c77173debe3e98cb744b855f10bd4941 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 7 Dec 2023 15:22:27 +0100 Subject: [PATCH 44/49] fix: tests --- .../lib/forest_admin_agent/utils/context_variables.rb | 2 +- .../forest_admin_agent/utils/context_variables_injector_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/utils/context_variables.rb b/packages/forest_admin_agent/lib/forest_admin_agent/utils/context_variables.rb index f0a692da4..6e9585a18 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/utils/context_variables.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/utils/context_variables.rb @@ -32,7 +32,7 @@ def get_current_user_data(context_variable_key) return user[:tags][context_variable_key[USER_VALUE_TAG_PREFIX.length..]] end - user[context_variable_key[USER_VALUE_PREFIX.length..].to_sym] + user[context_variable_key[USER_VALUE_PREFIX.length..]] end end end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_injector_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_injector_spec.rb index 22942274f..b0f074562 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_injector_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_injector_spec.rb @@ -11,7 +11,7 @@ module Utils 'firstName' => 'John', 'lastName' => 'Doe', 'fullName' => 'John Doe', - 'email' => 'John Doe', + 'email' => 'john.doe@domain.com', 'tags' => { 'planet' => 'Death Star' }, 'roleId' => 1, 'permissionLevel' => 'admin' From 12cf0976d14883d9561248efb4e01794d3307203 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 7 Dec 2023 15:57:04 +0100 Subject: [PATCH 45/49] fix(permissions): remove useless comment --- .../lib/forest_admin_agent/services/permissions.rb | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb index dc2005d5d..b632d2d9f 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb @@ -178,16 +178,7 @@ def sanitize_chart_parameters(parameters) parameters.select { |_, value| !value.nil? && value != '' } end - # protected function arrayHash(array $data): string - # { - # ArrayHelper::ksortRecursive($data); - # - # return sha1(json_encode($data, JSON_THROW_ON_ERROR)); - # stat_hash << "#{stat['type']}:#{Digest::SHA1.hexdigest(stat.sort.to_h.to_s)}" - # } def array_hash(data) - # data.deep_sort.to_s - Digest::SHA1.hexdigest(data.sort.to_h.to_s) end From 2675a19f8811e1f8650f200e0c84442b1473df48 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 7 Dec 2023 15:57:32 +0100 Subject: [PATCH 46/49] fix(context_variables): force symbolize keys fro user & team --- .../lib/forest_admin_agent/utils/context_variables.rb | 6 +++--- .../lib/forest_admin_agent/utils/context_variables_spec.rb | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/utils/context_variables.rb b/packages/forest_admin_agent/lib/forest_admin_agent/utils/context_variables.rb index 6e9585a18..a02c9337f 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/utils/context_variables.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/utils/context_variables.rb @@ -10,8 +10,8 @@ class ContextVariables USER_VALUE_TEAM_PREFIX = 'currentUser.team.'.freeze def initialize(team, user, request_context_variables = nil) - @team = team - @user = user + @team = team.transform_keys(&:to_sym) + @user = user.transform_keys(&:to_sym) @request_context_variables = request_context_variables end @@ -32,7 +32,7 @@ def get_current_user_data(context_variable_key) return user[:tags][context_variable_key[USER_VALUE_TAG_PREFIX.length..]] end - user[context_variable_key[USER_VALUE_PREFIX.length..]] + user[context_variable_key[USER_VALUE_PREFIX.length..].to_sym] end end end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_spec.rb index a54ba88c6..94c0e7125 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_spec.rb @@ -36,8 +36,8 @@ module Utils it 'returns the corresponding value from the key provided of the user data' do context_variables = described_class.new(team, user, request_context_variables) - # expect(context_variables.get_value('currentUser.firstName')).to eq('John') - # expect(context_variables.get_value('currentUser.tags.foo')).to eq('bar') + expect(context_variables.get_value('currentUser.firstName')).to eq('John') + expect(context_variables.get_value('currentUser.tags.foo')).to eq('bar') expect(context_variables.get_value('currentUser.team.id')).to eq(1) end end From 6e561fc659f122fd78f006f523e0034ea19f3c0d Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 8 Dec 2023 14:12:11 +0100 Subject: [PATCH 47/49] chore: remove lightly package & replace cache by file-cache package --- packages/forest_admin_agent/forest_admin_agent.gemspec | 1 - .../lib/forest_admin_agent/auth/oidc_client_manager.rb | 5 +++-- .../lib/forest_admin_agent/builder/agent_factory.rb | 10 +++++----- .../auth/oidc_client_manager_spec.rb | 4 ++-- .../forest_admin_agent/builder/agent_factory_spec.rb | 2 +- .../forest_admin_agent/services/permissions_spec.rb | 7 ++----- packages/forest_admin_agent/spec/spec_helper.rb | 6 +++--- 7 files changed, 16 insertions(+), 19 deletions(-) diff --git a/packages/forest_admin_agent/forest_admin_agent.gemspec b/packages/forest_admin_agent/forest_admin_agent.gemspec index 52cf16129..0d0801bed 100644 --- a/packages/forest_admin_agent/forest_admin_agent.gemspec +++ b/packages/forest_admin_agent/forest_admin_agent.gemspec @@ -41,7 +41,6 @@ admin work on any Ruby application." spec.add_dependency "jsonapi-serializers", "~> 1.0" spec.add_dependency "jwt", "~> 2.7" spec.add_dependency "ld-eventsource", "~> 2.2" - spec.add_dependency "lightly", "~> 0.4.0" spec.add_dependency "mono_logger", "~> 1.1" spec.add_dependency "openid_connect", "~> 2.2" spec.add_dependency "rake", "~> 13.0" diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/auth/oidc_client_manager.rb b/packages/forest_admin_agent/lib/forest_admin_agent/auth/oidc_client_manager.rb index 71070f634..a0adbbaaa 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/auth/oidc_client_manager.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/auth/oidc_client_manager.rb @@ -1,3 +1,4 @@ +require 'filecache' require 'openid_connect' require_relative 'oauth2/oidc_config' require_relative 'oauth2/forest_provider' @@ -18,8 +19,8 @@ def make_forest_provider(rendering_id) private def setup_cache(env_secret, config_agent) - lightly = Lightly.new(life: TTL, dir: "#{config_agent[:cache_dir]}/issuer") - lightly.get env_secret do + cache = FileCache.new('auth_issuer', (config_agent[:cache_dir]).to_s, TTL) + cache.get_or_set env_secret do oidc_config = retrieve_config(config_agent[:forest_server_url]) credentials = register( config_agent[:env_secret], diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb b/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb index 9183d4338..53b9d1c89 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb @@ -1,5 +1,5 @@ require 'dry-container' -require 'lightly' +require 'filecache' module ForestAdminAgent module Builder @@ -41,8 +41,8 @@ def send_schema(force: false) # Logger::log('Info', 'schema was updated, sending new version'); client = ForestAdminAgent::Http::ForestAdminApiRequester.new client.post('/forest/apimaps', schema.to_json) - schema_file_hash_cache = Lightly.new(life: TTL_SCHEMA, dir: @options[:cache_dir].to_s) - schema_file_hash_cache.get 'value' do + schema_file_hash_cache = FileCache.new('app', @options[:cache_dir].to_s, TTL_SCHEMA) + schema_file_hash_cache.get_or_set 'value' do schema[:meta][:schemaFileHash] end @container.register(:schema_file_hash, schema_file_hash_cache) @@ -59,11 +59,11 @@ def build_container end def build_cache - @container.register(:cache, Lightly.new(life: TTL_CONFIG, dir: @options[:cache_dir].to_s)) + @container.register(:cache, FileCache.new('app', @options[:cache_dir].to_s, TTL_SCHEMA)) return unless @has_env_secret cache = @container.resolve(:cache) - cache.get 'config' do + cache.get_or_set 'config' do @options.to_h end end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/auth/oidc_client_manager_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/auth/oidc_client_manager_spec.rb index 91fe99d2d..903d8642b 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/auth/oidc_client_manager_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/auth/oidc_client_manager_spec.rb @@ -11,8 +11,8 @@ module Auth let(:faraday_connection) { instance_double(Faraday::Connection) } before do - lightly = Lightly.new(dir: "#{Facades::Container.cache(:cache_dir)}/issuer") - lightly.flush + cache = FileCache.new('auth_issuer', Facades::Container.cache(:cache_dir).to_s) + cache.clear end context 'when then oidc is called and forest api is down' do diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/builder/agent_factory_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/builder/agent_factory_spec.rb index 95291441c..ec676fd90 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/builder/agent_factory_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/builder/agent_factory_spec.rb @@ -17,7 +17,7 @@ module Builder it 'build the cache' do expect(described_class.instance.container.resolve(:cache)).not_to be_nil - expect(described_class.instance.container.resolve(:cache)).to be_instance_of Lightly + expect(described_class.instance.container.resolve(:cache)).to be_instance_of FileCache end it 'build the logger' do diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/permissions_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/permissions_spec.rb index cdfa0386a..8c5b5e057 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/permissions_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/services/permissions_spec.rb @@ -32,11 +32,8 @@ module Services before do # clear main cache - cache = Lightly.new( - life: Facades::Container.config_from_cache[:permission_expiration], - dir: Facades::Container.config_from_cache[:cache_dir].to_s - ) - cache.clear 'config' + cache = FileCache.new('app', 'tmp/cache/forest_admin') + cache.clear @datasource = Datasource.new collection_book = instance_double( diff --git a/packages/forest_admin_agent/spec/spec_helper.rb b/packages/forest_admin_agent/spec/spec_helper.rb index 7f9b93d86..b0c9365da 100644 --- a/packages/forest_admin_agent/spec/spec_helper.rb +++ b/packages/forest_admin_agent/spec/spec_helper.rb @@ -1,4 +1,4 @@ -require 'lightly' +require 'filecache' require 'simplecov' require 'simplecov_json_formatter' require 'simplecov-html' @@ -31,8 +31,8 @@ # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |config| config.before do - lightly = Lightly.new(dir: 'tmp/cache/forest_admin') - lightly.flush + cache = FileCache.new('app', 'tmp/cache/forest_admin') + cache.clear agent_factory = ForestAdminAgent::Builder::AgentFactory.instance agent_factory.setup( From fc1eb14c7202cde3f34287193d0943ce5ee0f19d Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 8 Dec 2023 14:23:53 +0100 Subject: [PATCH 48/49] fix: test schema_emitter --- .../utils/schema/schema_emitter_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/schema_emitter_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/schema_emitter_spec.rb index 752df64a0..be65f9edc 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/schema_emitter_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/schema_emitter_spec.rb @@ -43,9 +43,9 @@ module Schema before do cache = ForestAdminAgent::Builder::AgentFactory.instance.container.resolve(:cache) config = cache.get('config') - cache.clear 'config' + cache.clear config[:is_production] = false - cache.get 'config' do + cache.get_or_set 'config' do config end end @@ -76,9 +76,9 @@ module Schema before do cache = ForestAdminAgent::Builder::AgentFactory.instance.container.resolve(:cache) config = cache.get('config') - cache.clear 'config' + cache.clear config[:is_production] = true - cache.get 'config' do + cache.get_or_set 'config' do config end end From 5e1ab07cba890d412a14b34641e878ee89bf5c9f Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 8 Dec 2023 15:13:22 +0100 Subject: [PATCH 49/49] refactor: count routes --- .../lib/forest_admin_agent/routes/resources/count.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/count.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/count.rb index 02d170157..cffdcfa57 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/count.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/count.rb @@ -18,7 +18,7 @@ def handle_request(args = {}) if @collection.is_countable? filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new( - condition_tree: ConditionTreeFactory.intersect([@permissions.get_scope(@collection)]) + condition_tree: @permissions.get_scope(@collection) ) aggregation = ForestAdminDatasourceToolkit::Components::Query::Aggregation.new(operation: 'Count') result = @collection.aggregate(@caller, filter, aggregation)