From cb221d1e877260db05470f2da9615fa23ceeb129 Mon Sep 17 00:00:00 2001
From: Rutger Wessels <rwessels@gitlab.com>
Date: Fri, 19 Jan 2024 21:08:46 +0000
Subject: [PATCH] Let CurrentSettings read from ApplicationSetting or
 OrganizationSetting

---
 .../organizations/organization_setting.rb     |  8 +++
 lib/gitlab/current_settings.rb                | 10 +++-
 lib/gitlab/fake_application_settings.rb       |  4 ++
 spec/lib/gitlab/current_settings_spec.rb      | 56 ++++++++++++++++++-
 .../organization_setting_spec.rb              | 38 +++++++++++++
 5 files changed, 111 insertions(+), 5 deletions(-)

diff --git a/app/models/organizations/organization_setting.rb b/app/models/organizations/organization_setting.rb
index 108531e670115..fb141c164d30f 100644
--- a/app/models/organizations/organization_setting.rb
+++ b/app/models/organizations/organization_setting.rb
@@ -2,6 +2,8 @@
 
 module Organizations
   class OrganizationSetting < ApplicationRecord
+    extend ::Organization::CurrentOrganization
+
     belongs_to :organization
 
     validates :settings, json_schema: { filename: "organization_settings" }
@@ -16,5 +18,11 @@ class OrganizationSetting < ApplicationRecord
         end
       end
     end
+
+    def self.for_current_organization
+      return unless current_organization
+
+      current_organization.settings || current_organization.build_settings
+    end
   end
 end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 64e0478734b61..a42b086319d9c 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -24,12 +24,18 @@ def expire_current_application_settings
         Gitlab::SafeRequestStore.delete(:current_application_settings)
       end
 
+      # rubocop:disable GitlabSecurity/PublicSend -- Method calls are forwarded to one of the setting classes
       def method_missing(name, *args, **kwargs, &block)
-        current_application_settings.send(name, *args, **kwargs, &block) # rubocop:disable GitlabSecurity/PublicSend
+        application_settings = current_application_settings
+
+        return application_settings.send(name, *args, **kwargs, &block) if application_settings.respond_to?(name)
+
+        Organizations::OrganizationSetting.for_current_organization.send(name, *args, **kwargs, &block)
       end
+      # rubocop:enable GitlabSecurity/PublicSend
 
       def respond_to_missing?(name, include_private = false)
-        current_application_settings.respond_to?(name, include_private) || super
+        current_application_settings.respond_to?(name, include_private) || Organizations::OrganizationSetting.for_current_organization.respond_to?(name, include_private) || super
       end
     end
   end
diff --git a/lib/gitlab/fake_application_settings.rb b/lib/gitlab/fake_application_settings.rb
index 0b9a4c161aeb1..da99d9b1b7063 100644
--- a/lib/gitlab/fake_application_settings.rb
+++ b/lib/gitlab/fake_application_settings.rb
@@ -46,6 +46,10 @@ def has_attribute?(key)
     def method_missing(*)
       nil
     end
+
+    def respond_to_missing?(*)
+      true
+    end
   end
 end
 
diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb
index 7fc50438c9574..17b2218e05c1c 100644
--- a/spec/lib/gitlab/current_settings_spec.rb
+++ b/spec/lib/gitlab/current_settings_spec.rb
@@ -88,6 +88,8 @@
   end
 
   describe '#current_application_settings', :use_clean_rails_memory_store_caching do
+    let_it_be(:organization_settings) { create(:organization_setting, restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]) }
+
     it 'allows keys to be called directly' do
       described_class.update!(home_page_url: 'http://mydomain.com', signup_enabled: false)
 
@@ -97,10 +99,58 @@
       expect(described_class.metrics_sample_interval).to be(15)
     end
 
-    it 'retrieves settings using ApplicationSettingFetcher' do
-      expect(Gitlab::ApplicationSettingFetcher).to receive(:current_application_settings).and_call_original
+    context 'when key is in ApplicationSettingFetcher' do
+      it 'retrieves settings using ApplicationSettingFetcher' do
+        expect(Gitlab::ApplicationSettingFetcher).to receive(:current_application_settings).and_call_original
+
+        described_class.home_page_url
+      end
+    end
+
+    context 'when key is in OrganizationSetting' do
+      before do
+        allow(Gitlab::ApplicationSettingFetcher).to receive(:current_application_settings).and_return(nil)
+      end
+
+      context 'and the current organization is known' do
+        before do
+          allow(Organizations::OrganizationSetting).to receive(:for_current_organization).and_return(organization_settings)
+        end
+
+        it 'retrieves settings using OrganizationSetting' do
+          expect(described_class.restricted_visibility_levels).to eq(organization_settings.restricted_visibility_levels)
+        end
+      end
+
+      context 'and the current organization is unknown' do
+        before do
+          allow(Organizations::OrganizationSetting).to receive(:for_current_organization).and_return(nil)
+        end
+
+        it 'raises NoMethodError' do
+          expect { described_class.foo }.to raise_error(NoMethodError)
+        end
+      end
+    end
+
+    context 'when key is in both sources' do
+      it 'for test purposes, ensure the values are different' do
+        expect(
+          Gitlab::ApplicationSettingFetcher.current_application_settings.restricted_visibility_levels
+        ).not_to eq(organization_settings.restricted_visibility_levels)
+      end
 
-      described_class.home_page_url
+      it 'prefers ApplicationSettingFetcher' do
+        expect(described_class.restricted_visibility_levels).to eq(
+          Gitlab::ApplicationSettingFetcher.current_application_settings.restricted_visibility_levels
+        )
+      end
+    end
+
+    context 'when key is in neither' do
+      it 'raises NoMethodError' do
+        expect { described_class.foo }.to raise_error(NoMethodError)
+      end
     end
   end
 
diff --git a/spec/models/organizations/organization_setting_spec.rb b/spec/models/organizations/organization_setting_spec.rb
index 376d0b7fe7709..dd90a4d0f58f4 100644
--- a/spec/models/organizations/organization_setting_spec.rb
+++ b/spec/models/organizations/organization_setting_spec.rb
@@ -4,6 +4,7 @@
 
 RSpec.describe Organizations::OrganizationSetting, type: :model, feature_category: :cell do
   let_it_be(:organization) { create(:organization) }
+  let_it_be(:organization_setting) { create(:organization_setting, organization: organization) }
 
   describe 'associations' do
     it { is_expected.to belong_to :organization }
@@ -54,4 +55,41 @@
       end
     end
   end
+
+  describe '.for_current_organization' do
+    let(:dummy_class) { Class.new { extend Organization::CurrentOrganization } }
+
+    subject(:settings) { described_class.for_current_organization }
+
+    context 'when there is no current organization' do
+      it { is_expected.to be_nil }
+    end
+
+    context 'when there is a current organization' do
+      before do
+        dummy_class.current_organization = organization
+      end
+
+      after do
+        dummy_class.current_organization = nil
+      end
+
+      it 'returns current organization' do
+        expect(settings).to eq(organization_setting)
+      end
+
+      context 'when current organization does not have settings' do
+        before do
+          allow(organization).to receive(:settings).and_return(nil)
+        end
+
+        it 'returns new settings record' do
+          new_settings = settings
+
+          expect(new_settings.organization).to eq(organization)
+          expect(new_settings.new_record?).to eq(true)
+        end
+      end
+    end
+  end
 end
-- 
GitLab