From 4d149016ce0f4da73de569f597bf8fe23227ed94 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20Ha=C3=9F?= <oss@hass.onl>
Date: Wed, 21 Feb 2024 18:36:49 +0000
Subject: [PATCH] Asciidoc max include application setting

---
 app/helpers/application_settings_helper.rb        |  3 ++-
 app/models/application_setting.rb                 |  3 +++
 app/models/application_setting_implementation.rb  |  3 ++-
 ...220225325_add_asciidoc_max_includes_setting.rb | 15 +++++++++++++++
 db/schema_migrations/20231220225325               |  1 +
 db/structure.sql                                  |  1 +
 doc/api/settings.md                               |  1 +
 doc/user/asciidoc.md                              | 10 ++++++++--
 lib/gitlab/asciidoc.rb                            |  3 +--
 spec/lib/gitlab/asciidoc_spec.rb                  |  4 ++--
 spec/models/application_setting_spec.rb           |  6 ++++++
 11 files changed, 42 insertions(+), 8 deletions(-)
 create mode 100644 db/migrate/20231220225325_add_asciidoc_max_includes_setting.rb
 create mode 100644 db/schema_migrations/20231220225325

diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 2af212fd75c80..d048bfe9ce041 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -515,7 +515,8 @@ def visible_attributes
       :project_jobs_api_rate_limit,
       :security_txt_content,
       :allow_project_creation_for_guest_and_below,
-      :downstream_pipeline_trigger_limit_per_project_user_sha
+      :downstream_pipeline_trigger_limit_per_project_user_sha,
+      :asciidoc_max_includes
     ].tap do |settings|
       next if Gitlab.com?
 
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index e80ad2cae3d5c..9022b190165ea 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -685,6 +685,9 @@ def self.kroki_formats_attributes
     length: { maximum: 2_048, message: N_('is too long (maximum is %{count} characters)') },
     allow_blank: true
 
+  validates :asciidoc_max_includes,
+    numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 64 }
+
   attr_encrypted :asset_proxy_secret_key,
     mode: :per_attribute_iv,
     key: Settings.attr_encrypted_db_key_base_truncated,
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index c3ff9c50e7646..7af420e7de467 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -281,7 +281,8 @@ def defaults # rubocop:disable Metrics/AbcSize
         allow_project_creation_for_guest_and_below: true,
         enable_member_promotion_management: false,
         security_approval_policies_limit: 5,
-        downstream_pipeline_trigger_limit_per_project_user_sha: 0
+        downstream_pipeline_trigger_limit_per_project_user_sha: 0,
+        asciidoc_max_includes: 32
       }.tap do |hsh|
         hsh.merge!(non_production_defaults) unless Rails.env.production?
       end
diff --git a/db/migrate/20231220225325_add_asciidoc_max_includes_setting.rb b/db/migrate/20231220225325_add_asciidoc_max_includes_setting.rb
new file mode 100644
index 0000000000000..e88143db5350f
--- /dev/null
+++ b/db/migrate/20231220225325_add_asciidoc_max_includes_setting.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddAsciidocMaxIncludesSetting < Gitlab::Database::Migration[2.2]
+  enable_lock_retries!
+
+  milestone '16.10'
+
+  def up
+    add_column :application_settings, :asciidoc_max_includes, :smallint, default: 32, null: false
+  end
+
+  def down
+    remove_column :application_settings, :asciidoc_max_includes
+  end
+end
diff --git a/db/schema_migrations/20231220225325 b/db/schema_migrations/20231220225325
new file mode 100644
index 0000000000000..498d46c69b149
--- /dev/null
+++ b/db/schema_migrations/20231220225325
@@ -0,0 +1 @@
+0202c688fe9d32260e6fdea5f13a06e4183eea4b0866199056cfcd347aaac263
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index fd9b1838fb92a..f3cb4536ffd01 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -4115,6 +4115,7 @@ CREATE TABLE application_settings (
     encrypted_arkose_labs_client_secret_iv bytea,
     duo_features_enabled boolean DEFAULT true NOT NULL,
     lock_duo_features_enabled boolean DEFAULT false NOT NULL,
+    asciidoc_max_includes smallint DEFAULT 32 NOT NULL,
     CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
     CONSTRAINT app_settings_container_registry_pre_import_tags_rate_positive CHECK ((container_registry_pre_import_tags_rate >= (0)::numeric)),
     CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)),
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 922cb2bd3c86d..ea40fdb5d9eb3 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -635,6 +635,7 @@ listed in the descriptions of the relevant settings.
 | `whats_new_variant`                      | string           | no                                   | What's new variant, possible values: `all_tiers`, `current_tier`, and `disabled`. |
 | `wiki_page_max_content_bytes`            | integer          | no                                   | Maximum wiki page content size in **bytes**. Default: 52428800 Bytes (50 MB). The minimum value is 1024 bytes. |
 | `bulk_import_concurrent_pipeline_batch_limit` | integer     | no                                   | Maximum simultaneous Direct Transfer batches to process. |
+| `asciidoc_max_includes`                  | integer          | no                                   | Maximum limit of AsciiDoc include directives being processed in any one document. Default: 32. Maximum: 64. |
 
 ### Configure inactive project deletion
 
diff --git a/doc/user/asciidoc.md b/doc/user/asciidoc.md
index 613c733635bc0..93976486f98b5 100644
--- a/doc/user/asciidoc.md
+++ b/doc/user/asciidoc.md
@@ -252,8 +252,14 @@ include::basics.adoc[]
 
 To guarantee good system performance and prevent malicious documents from causing
 problems, GitLab enforces a maximum limit on the number of include directives
-processed in any one document. You can include up to 32 documents, which is
-inclusive of transitive dependencies.
+processed in any one document. By default, a document can have up to 32 include directives, which is
+inclusive of transitive dependencies. To customize the number of processed includes directives, change
+the application setting `asciidoc_max_includes` with the
+[application settings API](../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls).
+
+NOTE:
+The current maximum allowed value for`asciidoc_max_includes` is 64. If the value is
+too high might cause performance issues in some situations.
 
 To use includes from separate pages or external URLs, enable the `allow-uri-read`
 in [application settings](../administration/wikis/index.md#allow-uri-includes-for-asciidoc).
diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb
index 31e8dcd84b78c..0d4649181d209 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -14,7 +14,6 @@ module Gitlab
   # the resulting HTML through HTML pipeline filters.
   module Asciidoc
     MAX_INCLUDE_DEPTH = 5
-    MAX_INCLUDES = 32
     DEFAULT_ADOC_ATTRS = {
         'showtitle' => true,
         'sectanchors' => true,
@@ -76,7 +75,7 @@ def self.render(input, context)
                         extensions: extensions }
 
       context[:pipeline] = :ascii_doc
-      context[:max_includes] = [MAX_INCLUDES, context[:max_includes]].compact.min
+      context[:max_includes] = [::Gitlab::CurrentSettings.asciidoc_max_includes, context[:max_includes]].compact.min
 
       Gitlab::Plantuml.configure
 
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index a43f08db65951..d07ee10cf7f29 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -852,9 +852,9 @@ def many_includes(target)
             ADOC
           end
 
-          context 'when the document includes more than MAX_INCLUDES' do
+          context 'when the document includes more than asciidoc_max_includes' do
             before do
-              stub_const("#{described_class}::MAX_INCLUDES", 2)
+              stub_application_setting(asciidoc_max_includes: 2)
             end
 
             it 'includes only the content of the first 2 sources' do
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index d88415527ad42..9cdb25019970b 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -30,6 +30,7 @@
     it { expect(setting.allow_project_creation_for_guest_and_below).to eq(true) }
     it { expect(setting.members_delete_limit).to eq(60) }
     it { expect(setting.downstream_pipeline_trigger_limit_per_project_user_sha).to eq(0) }
+    it { expect(setting.asciidoc_max_includes).to eq(32) }
   end
 
   describe 'validations' do
@@ -1665,4 +1666,9 @@ def expect_invalid
   context 'security txt content' do
     it { is_expected.to validate_length_of(:security_txt_content).is_at_most(2048) }
   end
+
+  context 'ascii max includes' do
+    it { is_expected.to validate_numericality_of(:asciidoc_max_includes).only_integer.is_greater_than_or_equal_to(0) }
+    it { is_expected.to validate_numericality_of(:asciidoc_max_includes).only_integer.is_less_than_or_equal_to(64) }
+  end
 end
-- 
GitLab