From 9f38ed6baee3d2dccfc78a63f9c86681c70f9620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javi=20Mart=C3=ADn?= Date: Fri, 25 Oct 2024 13:38:34 +0200 Subject: [PATCH 1/3] Extract method to stub secrets in tests --- .../account/sign_in_info_component_spec.rb | 4 +--- spec/mailers/mailer_spec.rb | 4 ++-- spec/models/tenant_spec.rb | 9 +++------ spec/models/user_spec.rb | 12 ++++++------ spec/support/common_actions.rb | 1 + spec/support/common_actions/secrets.rb | 7 +++++++ spec/system/users_auth_spec.rb | 4 +--- 7 files changed, 21 insertions(+), 20 deletions(-) create mode 100644 spec/support/common_actions/secrets.rb diff --git a/spec/components/account/sign_in_info_component_spec.rb b/spec/components/account/sign_in_info_component_spec.rb index 0e100d2d3..e11d4dd23 100644 --- a/spec/components/account/sign_in_info_component_spec.rb +++ b/spec/components/account/sign_in_info_component_spec.rb @@ -5,9 +5,7 @@ describe Account::SignInInfoComponent do context "Security secret for render last sign in is enabled" do it "shows a sign in info" do - allow(Rails.application).to receive(:secrets).and_return(ActiveSupport::OrderedOptions.new.merge( - security: { last_sign_in: true } - )) + stub_secrets(security: { last_sign_in: true }) render_inline Account::SignInInfoComponent.new(account) diff --git a/spec/mailers/mailer_spec.rb b/spec/mailers/mailer_spec.rb index aed829ae2..4f93ef388 100644 --- a/spec/mailers/mailer_spec.rb +++ b/spec/mailers/mailer_spec.rb @@ -85,12 +85,12 @@ describe Mailer do let(:super_settings) { { address: "super.consul.dev", username: "super" } } before do - allow(Rails.application).to receive(:secrets).and_return(ActiveSupport::OrderedOptions.new.merge( + stub_secrets( smtp_settings: default_settings, tenants: { supermailer: { smtp_settings: super_settings } } - )) + ) end it "does not overwrite the settings for the default tenant" do diff --git a/spec/models/tenant_spec.rb b/spec/models/tenant_spec.rb index e7244c226..f3746f0e7 100644 --- a/spec/models/tenant_spec.rb +++ b/spec/models/tenant_spec.rb @@ -243,10 +243,7 @@ describe Tenant do describe ".current_secrets" do context "same secrets for all tenants" do before do - allow(Rails.application).to receive(:secrets).and_return(ActiveSupport::OrderedOptions.new.merge( - star: "Sun", - volume: "Medium" - )) + stub_secrets(star: "Sun", volume: "Medium") end it "returns the default secrets for the default tenant" do @@ -266,11 +263,11 @@ describe Tenant do context "tenant overwriting secrets" do before do - allow(Rails.application).to receive(:secrets).and_return(ActiveSupport::OrderedOptions.new.merge( + stub_secrets( star: "Sun", volume: "Medium", tenants: { proxima: { star: "Alpha Centauri" }} - )) + ) end it "returns the default secrets for the default tenant" do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index b29c9a589..c8d6777c3 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -994,7 +994,7 @@ describe User do context "when secrets are configured" do before do - allow(Rails.application).to receive(:secrets).and_return(ActiveSupport::OrderedOptions.new.merge( + stub_secrets( security: { password_complexity: true }, @@ -1005,7 +1005,7 @@ describe User do } } } - )) + ) end it "uses the general secrets for the main tenant" do @@ -1027,7 +1027,7 @@ describe User do context "when secrets are configured" do before do - allow(Rails.application).to receive(:secrets).and_return(ActiveSupport::OrderedOptions.new.merge( + stub_secrets( security: { lockable: { maximum_attempts: "14" } }, @@ -1038,7 +1038,7 @@ describe User do } } } - )) + ) end it "uses the general secrets for the main tenant" do @@ -1060,7 +1060,7 @@ describe User do context "when secrets are configured" do before do - allow(Rails.application).to receive(:secrets).and_return(ActiveSupport::OrderedOptions.new.merge( + stub_secrets( security: { lockable: { unlock_in: "2" } }, @@ -1071,7 +1071,7 @@ describe User do } } } - )) + ) end it "uses the general secrets for the main tenant" do diff --git a/spec/support/common_actions.rb b/spec/support/common_actions.rb index 355ce41da..22910a166 100644 --- a/spec/support/common_actions.rb +++ b/spec/support/common_actions.rb @@ -14,6 +14,7 @@ module CommonActions include Polls include Proposals include RemoteCensusMock + include Secrets include Tags include Translations include Users diff --git a/spec/support/common_actions/secrets.rb b/spec/support/common_actions/secrets.rb new file mode 100644 index 000000000..4af9585f4 --- /dev/null +++ b/spec/support/common_actions/secrets.rb @@ -0,0 +1,7 @@ +module Secrets + def stub_secrets(configuration) + allow(Rails.application).to receive(:secrets).and_return( + ActiveSupport::OrderedOptions.new.merge(configuration) + ) + end +end diff --git a/spec/system/users_auth_spec.rb b/spec/system/users_auth_spec.rb index 936b035cd..5c7d35953 100644 --- a/spec/system/users_auth_spec.rb +++ b/spec/system/users_auth_spec.rb @@ -661,9 +661,7 @@ describe "Users" do context "Regular authentication with password complexity enabled" do before do - allow(Rails.application).to receive(:secrets).and_return(ActiveSupport::OrderedOptions.new.merge( - security: { password_complexity: true } - )) + stub_secrets(security: { password_complexity: true }) end context "Sign up" do From 07202fea10b79a32d2bbe716c09adf737827d7f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javi=20Mart=C3=ADn?= Date: Wed, 30 Oct 2024 15:55:29 +0100 Subject: [PATCH 2/3] Add and apply Style/RedundantBegin rubocop rule We're about to add code which might fall into the `RedundantBegin` category, so we're adding the rule in order to prevent that. --- .rubocop.yml | 3 + app/controllers/graphql_controller.rb | 34 ++++--- app/models/legislation/annotation.rb | 24 +++-- app/models/machine_learning.rb | 94 +++++++++---------- config/deploy.rb | 34 +++---- .../officing/voters_controller_spec.rb | 12 +-- .../polls/answers_controller_spec.rb | 14 ++- spec/models/poll/answer_spec.rb | 6 +- spec/support/common_actions/graphql_api.rb | 12 +-- 9 files changed, 109 insertions(+), 124 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 12c9957b4..bd287aa83 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -781,6 +781,9 @@ Style/RaiseArgs: Style/RedundantArgument: Enabled: true +Style/RedundantBegin: + Enabled: true + Style/RedundantCondition: Enabled: true diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 0c64ddfb9..0a13ac349 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -9,25 +9,23 @@ class GraphqlController < ApplicationController class QueryStringError < StandardError; end def execute - begin - raise GraphqlController::QueryStringError if query_string.nil? + raise GraphqlController::QueryStringError if query_string.nil? - result = ConsulSchema.execute( - query_string, - variables: prepare_variables, - context: {}, - operation_name: params[:operationName] - ) - render json: result - rescue GraphqlController::QueryStringError - render json: { message: "Query string not present" }, status: :bad_request - rescue JSON::ParserError - render json: { message: "Error parsing JSON" }, status: :bad_request - rescue GraphQL::ParseError - render json: { message: "Query string is not valid JSON" }, status: :bad_request - rescue ArgumentError => e - render json: { message: e.message }, status: :bad_request - end + result = ConsulSchema.execute( + query_string, + variables: prepare_variables, + context: {}, + operation_name: params[:operationName] + ) + render json: result + rescue GraphqlController::QueryStringError + render json: { message: "Query string not present" }, status: :bad_request + rescue JSON::ParserError + render json: { message: "Error parsing JSON" }, status: :bad_request + rescue GraphQL::ParseError + render json: { message: "Query string is not valid JSON" }, status: :bad_request + rescue ArgumentError => e + render json: { message: e.message }, status: :bad_request end private diff --git a/app/models/legislation/annotation.rb b/app/models/legislation/annotation.rb index c9fb4d046..b1417a886 100644 --- a/app/models/legislation/annotation.rb +++ b/app/models/legislation/annotation.rb @@ -25,23 +25,21 @@ class Legislation::Annotation < ApplicationRecord end def store_context - begin - html = draft_version.body_html - doc = Nokogiri::HTML(html) + html = draft_version.body_html + doc = Nokogiri::HTML(html) - selector_start = "/html/body/#{range_start}" - el_start = doc.at_xpath(selector_start) + selector_start = "/html/body/#{range_start}" + el_start = doc.at_xpath(selector_start) - selector_end = "/html/body/#{range_end}" - el_end = doc.at_xpath(selector_end) + selector_end = "/html/body/#{range_end}" + el_end = doc.at_xpath(selector_end) - remainder_el_start = el_start.text[0..range_start_offset - 1] unless range_start_offset.zero? - remainder_el_end = el_end.text[range_end_offset..-1] + remainder_el_start = el_start.text[0..range_start_offset - 1] unless range_start_offset.zero? + remainder_el_end = el_end.text[range_end_offset..-1] - self.context = "#{remainder_el_start}#{quote}#{remainder_el_end}" - rescue - "#{quote}" - end + self.context = "#{remainder_el_start}#{quote}#{remainder_el_end}" + rescue + "#{quote}" end def create_first_comment diff --git a/app/models/machine_learning.rb b/app/models/machine_learning.rb index 5541d3327..34327bd0d 100644 --- a/app/models/machine_learning.rb +++ b/app/models/machine_learning.rb @@ -15,57 +15,55 @@ class MachineLearning end def run - begin - export_proposals_to_json - export_budget_investments_to_json - export_comments_to_json + export_proposals_to_json + export_budget_investments_to_json + export_comments_to_json - return unless run_machine_learning_scripts + return unless run_machine_learning_scripts - if updated_file?(MachineLearning.proposals_taggings_filename) && - updated_file?(MachineLearning.proposals_tags_filename) - cleanup_proposals_tags! - import_ml_proposals_tags - update_machine_learning_info_for("tags") - end - - if updated_file?(MachineLearning.investments_taggings_filename) && - updated_file?(MachineLearning.investments_tags_filename) - cleanup_investments_tags! - import_ml_investments_tags - update_machine_learning_info_for("tags") - end - - if updated_file?(MachineLearning.proposals_related_filename) - cleanup_proposals_related_content! - import_proposals_related_content - update_machine_learning_info_for("related_content") - end - - if updated_file?(MachineLearning.investments_related_filename) - cleanup_investments_related_content! - import_budget_investments_related_content - update_machine_learning_info_for("related_content") - end - - if updated_file?(MachineLearning.proposals_comments_summary_filename) - cleanup_proposals_comments_summary! - import_ml_proposals_comments_summary - update_machine_learning_info_for("comments_summary") - end - - if updated_file?(MachineLearning.investments_comments_summary_filename) - cleanup_investments_comments_summary! - import_ml_investments_comments_summary - update_machine_learning_info_for("comments_summary") - end - - job.update!(finished_at: Time.current) - Mailer.machine_learning_success(user).deliver_later - rescue Exception => e - handle_error(e) - raise e + if updated_file?(MachineLearning.proposals_taggings_filename) && + updated_file?(MachineLearning.proposals_tags_filename) + cleanup_proposals_tags! + import_ml_proposals_tags + update_machine_learning_info_for("tags") end + + if updated_file?(MachineLearning.investments_taggings_filename) && + updated_file?(MachineLearning.investments_tags_filename) + cleanup_investments_tags! + import_ml_investments_tags + update_machine_learning_info_for("tags") + end + + if updated_file?(MachineLearning.proposals_related_filename) + cleanup_proposals_related_content! + import_proposals_related_content + update_machine_learning_info_for("related_content") + end + + if updated_file?(MachineLearning.investments_related_filename) + cleanup_investments_related_content! + import_budget_investments_related_content + update_machine_learning_info_for("related_content") + end + + if updated_file?(MachineLearning.proposals_comments_summary_filename) + cleanup_proposals_comments_summary! + import_ml_proposals_comments_summary + update_machine_learning_info_for("comments_summary") + end + + if updated_file?(MachineLearning.investments_comments_summary_filename) + cleanup_investments_comments_summary! + import_ml_investments_comments_summary + update_machine_learning_info_for("comments_summary") + end + + job.update!(finished_at: Time.current) + Mailer.machine_learning_success(user).deliver_later + rescue Exception => e + handle_error(e) + raise e end handle_asynchronously :run, queue: "machine_learning" diff --git a/config/deploy.rb b/config/deploy.rb index 265dcd95a..84d1e6fcc 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -84,18 +84,16 @@ end task :install_ruby do on roles(:app) do within release_path do - begin - current_ruby = capture(:rvm, "current") - rescue SSHKit::Command::Failed + current_ruby = capture(:rvm, "current") + rescue SSHKit::Command::Failed + after "install_ruby", "rvm1:install:rvm" + after "install_ruby", "rvm1:install:ruby" + else + if current_ruby.include?("not installed") after "install_ruby", "rvm1:install:rvm" after "install_ruby", "rvm1:install:ruby" else - if current_ruby.include?("not installed") - after "install_ruby", "rvm1:install:rvm" - after "install_ruby", "rvm1:install:ruby" - else - info "Ruby: Using #{current_ruby}" - end + info "Ruby: Using #{current_ruby}" end end end @@ -104,19 +102,17 @@ end task :install_node do on roles(:app) do with rails_env: fetch(:rails_env) do + execute fetch(:fnm_install_node_command) + rescue SSHKit::Command::Failed begin - execute fetch(:fnm_install_node_command) + execute fetch(:fnm_setup_command) rescue SSHKit::Command::Failed - begin - execute fetch(:fnm_setup_command) - rescue SSHKit::Command::Failed - execute fetch(:fnm_install_command) - else - execute fetch(:fnm_update_command) - end - - execute fetch(:fnm_install_node_command) + execute fetch(:fnm_install_command) + else + execute fetch(:fnm_update_command) end + + execute fetch(:fnm_install_node_command) end end end diff --git a/spec/controllers/officing/voters_controller_spec.rb b/spec/controllers/officing/voters_controller_spec.rb index 81c9c6120..e1a611755 100644 --- a/spec/controllers/officing/voters_controller_spec.rb +++ b/spec/controllers/officing/voters_controller_spec.rb @@ -11,13 +11,11 @@ describe Officing::VotersController do 2.times.map do Thread.new do - begin - post :create, params: { - voter: { poll_id: poll.id, user_id: user.id }, - format: :js - } - rescue ActionDispatch::IllegalStateError - end + post :create, params: { + voter: { poll_id: poll.id, user_id: user.id }, + format: :js + } + rescue ActionDispatch::IllegalStateError end end.each(&:join) diff --git a/spec/controllers/polls/answers_controller_spec.rb b/spec/controllers/polls/answers_controller_spec.rb index 08f129c0b..171fc1cc8 100644 --- a/spec/controllers/polls/answers_controller_spec.rb +++ b/spec/controllers/polls/answers_controller_spec.rb @@ -8,14 +8,12 @@ describe Polls::AnswersController do 2.times.map do Thread.new do - begin - post :create, params: { - question_id: question.id, - option_id: question.question_options.find_by(title: "Answer A").id, - format: :js - } - rescue ActionDispatch::IllegalStateError, ActiveRecord::RecordInvalid - end + post :create, params: { + question_id: question.id, + option_id: question.question_options.find_by(title: "Answer A").id, + format: :js + } + rescue ActionDispatch::IllegalStateError, ActiveRecord::RecordInvalid end end.each(&:join) diff --git a/spec/models/poll/answer_spec.rb b/spec/models/poll/answer_spec.rb index b06ccbcd7..4f3ca154e 100644 --- a/spec/models/poll/answer_spec.rb +++ b/spec/models/poll/answer_spec.rb @@ -187,10 +187,8 @@ describe Poll::Answer do [answer, other_answer].map do |poll_answer| Thread.new do - begin - poll_answer.save_and_record_voter_participation - rescue ActiveRecord::RecordInvalid - end + poll_answer.save_and_record_voter_participation + rescue ActiveRecord::RecordInvalid end end.each(&:join) diff --git a/spec/support/common_actions/graphql_api.rb b/spec/support/common_actions/graphql_api.rb index 6deceb138..452433f87 100644 --- a/spec/support/common_actions/graphql_api.rb +++ b/spec/support/common_actions/graphql_api.rb @@ -18,14 +18,12 @@ module GraphQLAPI def extract_fields(response, collection_name, field_chain) fields = field_chain.split(".") dig(response, "data.#{collection_name}.edges").map do |node| - begin - if fields.size > 1 - node["node"][fields.first][fields.second] - else - node["node"][fields.first] - end - rescue NoMethodError + if fields.size > 1 + node["node"][fields.first][fields.second] + else + node["node"][fields.first] end + rescue NoMethodError end.compact end end From 424cedc0c8569e9702ed6a62e9992211ac61263c Mon Sep 17 00:00:00 2001 From: CoslaJohn Date: Mon, 22 Jul 2024 17:15:39 +0100 Subject: [PATCH 3/3] Restrict access to admin functions by IP There are many possible ways to implement this feature: * Adding a custom middleware * Using rack-attack with a blocklist * Using routes constraints We're choosing to use a controller concern with a redirect because it's what we do to handle unauthorized cancancan exceptions. --- app/controllers/admin/base_controller.rb | 1 + app/controllers/concerns/ip_denied_handler.rb | 17 ++++ app/lib/restrict_admin_ips.rb | 37 +++++++++ config/locales/en/general.yml | 2 + config/locales/es/general.yml | 2 + config/secrets.yml.example | 4 + .../controllers/admin/base_controller_spec.rb | 31 +++++++ spec/lib/restrict_admin_ips_spec.rb | 81 +++++++++++++++++++ 8 files changed, 175 insertions(+) create mode 100644 app/controllers/concerns/ip_denied_handler.rb create mode 100644 app/lib/restrict_admin_ips.rb create mode 100644 spec/controllers/admin/base_controller_spec.rb create mode 100644 spec/lib/restrict_admin_ips_spec.rb diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index 633668e7a..797ded423 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -1,4 +1,5 @@ class Admin::BaseController < ApplicationController + include IpDeniedHandler layout "admin" before_action :authenticate_user! diff --git a/app/controllers/concerns/ip_denied_handler.rb b/app/controllers/concerns/ip_denied_handler.rb new file mode 100644 index 000000000..3a2e9acd5 --- /dev/null +++ b/app/controllers/concerns/ip_denied_handler.rb @@ -0,0 +1,17 @@ +module IpDeniedHandler + extend ActiveSupport::Concern + + included do + before_action :restrict_ip, unless: :allowed_ip? + end + +private + + def restrict_ip + redirect_to root_path, alert: t("ip_denied_handler.unauthorized") + end + + def allowed_ip? + RestrictAdminIps.new(request.remote_ip).allowed? + end +end diff --git a/app/lib/restrict_admin_ips.rb b/app/lib/restrict_admin_ips.rb new file mode 100644 index 000000000..6c00f8dff --- /dev/null +++ b/app/lib/restrict_admin_ips.rb @@ -0,0 +1,37 @@ +class RestrictAdminIps + attr_reader :ip + + def initialize(ip) + @ip = ip + end + + def allowed? + unrestricted_access? || allowed_ip? + end + + private + + def unrestricted_access? + allowed_ips.blank? + end + + def allowed_ips + Array(Tenant.current_secrets.dig(:security, :allowed_admin_ips)) + end + + def allowed_ip? + normalized_allowed_ips.any? { |allowed_ip| allowed_ip.include?(ip) } + rescue IPAddr::Error + false + end + + def normalized_allowed_ips + allowed_ips.map do |allowed_ip| + IPAddr.new(allowed_ip) + rescue IPAddr::Error + Rails.logger.warn "Your allowed_admin_ips configuration includes the " \ + "address \"#{allowed_ip}\", which is not valid" + nil + end.compact + end +end diff --git a/config/locales/en/general.yml b/config/locales/en/general.yml index 3137ea334..5168cbd9d 100644 --- a/config/locales/en/general.yml +++ b/config/locales/en/general.yml @@ -759,6 +759,8 @@ en: youtube: "%{org} YouTube" telegram: "%{org} Telegram" instagram: "%{org} Instagram" + ip_denied_handler: + unauthorized: "Access denied. Your IP address is not allowed." unauthorized: default: You do not have permission to access this page. manage: diff --git a/config/locales/es/general.yml b/config/locales/es/general.yml index 0e658e9ec..e8bf22da2 100644 --- a/config/locales/es/general.yml +++ b/config/locales/es/general.yml @@ -759,6 +759,8 @@ es: youtube: "YouTube de %{org}" telegram: "Telegram de %{org}" instagram: "Instagram de %{org}" + ip_denied_handler: + unauthorized: "Acceso denegado. Tu IP no tiene permiso para ver este contenido." unauthorized: default: No tienes permiso para acceder a esta página. manage: diff --git a/config/secrets.yml.example b/config/secrets.yml.example index b785db667..4f298b089 100644 --- a/config/secrets.yml.example +++ b/config/secrets.yml.example @@ -23,6 +23,7 @@ development: devise_lockable: false multitenancy: false security: + # allowed_admin_ips: ["123.45.67.89", "192.168.1.0/24"] last_sign_in: false password_complexity: false # lockable: @@ -64,6 +65,7 @@ staging: managers_application_key: "" multitenancy: false security: + # allowed_admin_ips: ["123.45.67.89", "192.168.1.0/24"] last_sign_in: false password_complexity: false # lockable: @@ -118,6 +120,7 @@ preproduction: managers_application_key: "" multitenancy: false security: + # allowed_admin_ips: ["123.45.67.89", "192.168.1.0/24"] last_sign_in: false password_complexity: false # lockable: @@ -171,6 +174,7 @@ production: managers_application_key: "" multitenancy: false security: + # allowed_admin_ips: ["123.45.67.89", "192.168.1.0/24"] last_sign_in: false password_complexity: false # lockable: diff --git a/spec/controllers/admin/base_controller_spec.rb b/spec/controllers/admin/base_controller_spec.rb new file mode 100644 index 000000000..a869c4cc8 --- /dev/null +++ b/spec/controllers/admin/base_controller_spec.rb @@ -0,0 +1,31 @@ +require "rails_helper" + +describe Admin::BaseController, :admin do + controller do + def index + render plain: "Index" + end + end + + describe "#restrict_ip" do + before do + stub_secrets(security: { allowed_admin_ips: ["1.2.3.4", "5.6.7.8"] }) + end + + it "renders the content when the IP is allowed" do + request.env["REMOTE_ADDR"] = "1.2.3.4" + get :index + + expect(response).to be_successful + expect(response.body).to eq "Index" + end + + it "redirects to the root path when the IP isn't allowed" do + request.env["REMOTE_ADDR"] = "9.10.11.12" + get :index + + expect(response).to redirect_to root_path + expect(flash[:alert]).to eq "Access denied. Your IP address is not allowed." + end + end +end diff --git a/spec/lib/restrict_admin_ips_spec.rb b/spec/lib/restrict_admin_ips_spec.rb new file mode 100644 index 000000000..e8f6a2f9d --- /dev/null +++ b/spec/lib/restrict_admin_ips_spec.rb @@ -0,0 +1,81 @@ +require "rails_helper" + +describe RestrictAdminIps do + it "allows any IP when allowed_admin_ips isn't configured" do + stub_secrets(security: {}) + + expect(RestrictAdminIps.new("1.2.3.4")).to be_allowed + expect(RestrictAdminIps.new("whatever")).to be_allowed + end + + it "allows any IP when allowed_admin_ips is empty" do + stub_secrets(security: { allowed_admin_ips: [] }) + + expect(RestrictAdminIps.new("1.2.3.4")).to be_allowed + expect(RestrictAdminIps.new("whatever")).to be_allowed + end + + it "only allows IPs present in allowed_admin_ips" do + stub_secrets(security: { allowed_admin_ips: ["1.2.3.4", "5.6.7.8"] }) + + expect(RestrictAdminIps.new("1.2.3.4")).to be_allowed + expect(RestrictAdminIps.new("5.6.7.8")).to be_allowed + expect(RestrictAdminIps.new("9.9.9.9")).not_to be_allowed + expect(RestrictAdminIps.new("whatever")).not_to be_allowed + end + + it "restricts every IP when there are only malformed IPs on the list" do + stub_secrets(security: { allowed_admin_ips: ["not_an_ip"] }) + + expect(RestrictAdminIps.new("1.2.3.4")).not_to be_allowed + expect(RestrictAdminIps.new("not_an_ip")).not_to be_allowed + end + + it "ignores malformed IPs in the allowed_admin_ips list" do + stub_secrets(security: { allowed_admin_ips: ["1.2.3.4", "not_an_ip"] }) + + expect(RestrictAdminIps.new("1.2.3.4")).to be_allowed + expect(RestrictAdminIps.new("not_an_ip")).not_to be_allowed + end + + it "supports ranges of IPs" do + stub_secrets(security: { allowed_admin_ips: ["1.2.3.0/16"] }) + + expect(RestrictAdminIps.new("1.2.3.4")).to be_allowed + expect(RestrictAdminIps.new("1.2.3.5")).to be_allowed + expect(RestrictAdminIps.new("5.6.7.8")).not_to be_allowed + end + + context "tenant overwriting secrets" do + before do + stub_secrets({ + security: { + allowed_admin_ips: ["1.2.3.4", "5.6.7.8"] + }, + tenants: { + private: { + security: { + allowed_admin_ips: ["127.0.0.1", "192.168.1.1"] + } + } + } + }) + end + + it "uses the general secrets for the main tenant" do + expect(RestrictAdminIps.new("1.2.3.4")).to be_allowed + expect(RestrictAdminIps.new("5.6.7.8")).to be_allowed + expect(RestrictAdminIps.new("127.0.0.1")).not_to be_allowed + expect(RestrictAdminIps.new("192.168.1.1")).not_to be_allowed + end + + it "uses the tenant secrets for a tenant" do + allow(Tenant).to receive(:current_schema).and_return("private") + + expect(RestrictAdminIps.new("127.0.0.1")).to be_allowed + expect(RestrictAdminIps.new("192.168.1.1")).to be_allowed + expect(RestrictAdminIps.new("1.2.3.4")).not_to be_allowed + expect(RestrictAdminIps.new("5.6.7.8")).not_to be_allowed + end + end +end