Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-05-20 21:07:29 +00:00
parent c90ed875f9
commit 0ee3481b95
14 changed files with 348 additions and 20 deletions

View File

@ -450,6 +450,7 @@ module ApplicationSettingsHelper
:group_export_limit,
:group_download_export_limit,
:wiki_page_max_content_bytes,
:wiki_asciidoc_allow_uri_includes,
:container_registry_delete_tags_service_timeout,
:rate_limiting_response_text,
:package_registry_cleanup_policies_worker_capacity,

View File

@ -379,6 +379,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
validates :snippet_size_limit, numericality: { only_integer: true, greater_than: 0 }
validates :wiki_page_max_content_bytes, numericality: { only_integer: true, greater_than_or_equal_to: 1.kilobytes }
validates :wiki_asciidoc_allow_uri_includes, inclusion: { in: [true, false], message: N_('must be a boolean value') }
validates :max_yaml_size_bytes, numericality: { only_integer: true, greater_than: 0 }, presence: true
validates :max_yaml_depth, numericality: { only_integer: true, greater_than: 0 }, presence: true

View File

@ -223,6 +223,7 @@ module ApplicationSettingImplementation
user_show_add_ssh_key_message: true,
valid_runner_registrars: VALID_RUNNER_REGISTRAR_TYPES,
wiki_page_max_content_bytes: 50.megabytes,
wiki_asciidoc_allow_uri_includes: false,
package_registry_cleanup_policies_worker_capacity: 2,
container_registry_delete_tags_service_timeout: 250,
container_registry_expiration_policies_worker_capacity: 4,

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
class AddWikiAsciidocAllowUriIncludes < Gitlab::Database::Migration[2.1]
enable_lock_retries!
def change
add_column :application_settings, :wiki_asciidoc_allow_uri_includes, :boolean, default: false, null: false
end
end

View File

@ -0,0 +1 @@
676433c9330c304524c444c3d630558c849654173cd78f7e499087569203b7eb

View File

@ -11834,6 +11834,7 @@ CREATE TABLE application_settings (
encrypted_anthropic_api_key_iv bytea,
allow_account_deletion boolean DEFAULT true NOT NULL,
vertex_project text,
wiki_asciidoc_allow_uri_includes boolean DEFAULT false 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)),

View File

@ -82,6 +82,51 @@ so you should keep your wiki repositories as compact as possible.
For more information about tools to compact repositories,
read the documentation on [reducing repository size](../../user/project/repository/reducing_the_repo_size_using_git.md).
## Allow URI includes for AsciiDoc
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/348687) in GitLab 16.X (TBD)
Include directives import content from separate pages or external URLs,
and display them as part of the content of the current document. To enable
AsciiDoc includes, enable the feature through the Rails console or the API.
### Through the Rails console
To configure this setting through the Rails console:
1. Start the Rails console:
```shell
# For Omnibus installations
sudo gitlab-rails console
# For installations from source
sudo -u git -H bundle exec rails console -e production
```
1. Update the wiki to allow URI includes for AsciiDoc:
```ruby
ApplicationSetting.first.update!(wiki_asciidoc_allow_uri_includes: true)
```
To check if includes are enabled, start the Rails console and run:
```ruby
Gitlab::CurrentSettings.wiki_asciidoc_allow_uri_includes
```
### Through the API
To set the wiki to allow URI includes for AsciiDoc through the
[Application Settings API](../../api/settings.md#change-application-settings),
use a `curl` command:
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/application/settings?wiki_asciidoc_allow_uri_includes=true"
```
## Related topics
- [User documentation for wikis](../../user/project/wiki/index.md)

View File

@ -182,6 +182,7 @@ module API
optional :issues_create_limit, type: Integer, desc: "Maximum number of issue creation requests allowed per minute per user. Set to 0 for unlimited requests per minute."
optional :raw_blob_request_limit, type: Integer, desc: "Maximum number of requests per minute for each raw path. Set to 0 for unlimited requests per minute."
optional :wiki_page_max_content_bytes, type: Integer, desc: "Maximum wiki page content size in bytes"
optional :wiki_asciidoc_allow_uri_includes, type: Boolean, desc: "Allow URI includes for AsciiDoc wiki pages"
optional :require_admin_approval_after_user_signup, type: Boolean, desc: 'Require explicit admin approval for new signups'
optional :whats_new_variant, type: String, values: ApplicationSetting.whats_new_variants.keys, desc: "What's new variant, possible values: `all_tiers`, `current_tier`, and `disabled`."
optional :floc_enabled, type: Grape::API::Boolean, desc: 'Enable FloC (Federated Learning of Cohorts)'

View File

@ -70,7 +70,8 @@ module Gitlab
.merge({
# Define the Kroki server URL from the settings.
# This attribute cannot be overridden from the AsciiDoc document.
'kroki-server-url' => Gitlab::CurrentSettings.kroki_url
'kroki-server-url' => Gitlab::CurrentSettings.kroki_url,
'allow-uri-read' => Gitlab::CurrentSettings.wiki_asciidoc_allow_uri_includes
}),
extensions: extensions }

View File

@ -9,6 +9,8 @@ module Gitlab
class IncludeProcessor < Asciidoctor::IncludeExt::IncludeProcessor
extend ::Gitlab::Utils::Override
NoData = Class.new(StandardError)
def initialize(context)
super(logger: Gitlab::AppLogger)
@ -16,6 +18,7 @@ module Gitlab
@repository = context[:repository] || context[:project].try(:repository)
@max_includes = context[:max_includes].to_i
@included = []
@included_content = {}
# Note: Asciidoctor calls #freeze on extensions, so we can't set new
# instance variables after initialization.
@ -31,9 +34,10 @@ module Gitlab
doc = reader.document
max_include_depth = doc.attributes.fetch('max-include-depth').to_i
allow_uri_read = doc.attributes.fetch('allow-uri-read', false)
return false if max_include_depth < 1
return false if target_http?(target)
return false if target_http?(target) && !allow_uri_read
return false if included.size >= max_includes
true
@ -42,6 +46,7 @@ module Gitlab
override :resolve_target_path
def resolve_target_path(target, reader)
return unless repository.try(:exists?)
return target if target_http?(target)
base_path = reader.include_stack.empty? ? requested_path : reader.file
path = resolve_relative_path(target, base_path)
@ -51,12 +56,15 @@ module Gitlab
override :read_lines
def read_lines(filename, selector)
blob = read_blob(ref, filename)
content = read_content(filename)
raise NoData, filename if content.nil?
included << filename
if selector
blob.data.each_line.select.with_index(1, &selector)
content.each_line.select.with_index(1, &selector)
else
blob.data
content.lines
end
end
@ -67,7 +75,17 @@ module Gitlab
private
attr_reader :context, :repository, :cache, :max_includes, :included
attr_reader :context, :repository, :cache, :max_includes, :included, :included_content
def read_content(filename)
return included_content[filename] if included_content.key?(filename)
included_content[filename] = if target_http?(filename)
read_uri(filename)
else
read_blob(ref, filename)
end
end
# Gets a Blob at a path for a specific revision.
# This method will check that the Blob exists and contains readable text.
@ -75,16 +93,22 @@ module Gitlab
# revision - The String SHA1.
# path - The String file path.
#
# Returns a Blob
# Returns a string containing the blob content
def read_blob(ref, filename)
blob = repository&.blob_at(ref, filename)
raise 'Blob not found' unless blob
raise 'File is not readable' unless blob.readable_text?
raise NoData, 'Blob not found' unless blob
raise NoData, 'File is not readable' unless blob.readable_text?
included << filename
blob.data
end
blob
def read_uri(uri)
r = Gitlab::HTTP.get(uri)
raise NoData, uri unless r.success?
r.body
end
# Resolves the given relative path of file in repository into canonical

View File

@ -18,32 +18,174 @@ RSpec.describe Gitlab::Asciidoc::IncludeProcessor do
let(:max_includes) { 10 }
let(:reader) { Asciidoctor::PreprocessorReader.new(document, lines, 'file.adoc') }
let(:document) { Asciidoctor::Document.new(lines) }
subject(:processor) { described_class.new(processor_context) }
let(:a_blob) { double(:Blob, readable_text?: true, data: a_data) }
let(:a_data) { StringIO.new('include::b.adoc[]') }
let(:a_data) { 'include::b.adoc[]' }
let(:lines) { [':max-include-depth: 1000'] + Array.new(10, 'include::a.adoc[]') }
let(:directives) { [':max-include-depth: 1000'] }
let(:lines) { directives + Array.new(10, 'include::a.adoc[]') }
before do
allow(project.repository).to receive(:blob_at).with(ref, anything).and_return(nil)
allow(project.repository).to receive(:blob_at).with(ref, 'a.adoc').and_return(a_blob)
end
describe 'read_lines' do
let(:result) { processor.send(:read_lines, filename, selector) }
let(:selector) { nil }
context 'when reading a file in the repository' do
let(:filename) { 'a.adoc' }
it 'returns the blob contents' do
expect(result).to match_array([a_data])
end
context 'when the blob does not exist' do
let(:filename) { 'this-file-does-not-exist' }
it 'raises NoData' do
expect { result }.to raise_error(described_class::NoData)
end
end
context 'when there is a selector' do
let(:a_data) { %w[a b c d].join("\n") }
let(:selector) { ->(_, lineno) { lineno.odd? } }
it 'selects the lines' do
expect(result).to eq %W[a\n c\n]
end
end
it 'allows at most N blob includes' do
max_includes.times do
processor.send(:read_lines, filename, selector)
end
expect(processor.send(:include_allowed?, 'anything', reader)).to be_falsey
end
end
context 'when reading content from a URL' do
let(:filename) { 'http://example.org/file' }
it 'fetches the data using a GET request' do
stub_request(:get, filename).to_return(status: 200, body: 'something')
expect(result).to match_array(['something'])
end
context 'when the URI returns 404' do
before do
stub_request(:get, filename).to_return(status: 404, body: 'not found')
end
it 'raises NoData' do
expect { result }.to raise_error(described_class::NoData)
end
end
it 'allows at most N HTTP includes' do
stub_request(:get, filename).to_return(status: 200, body: 'something')
max_includes.times do
processor.send(:read_lines, filename, selector)
end
expect(processor.send(:include_allowed?, 'anything', reader)).to be_falsey
end
context 'when there is a selector' do
let(:http_body) { %w[x y z].join("\n") }
let(:selector) { ->(_, lineno) { lineno.odd? } }
it 'selects the lines' do
stub_request(:get, filename).to_return(status: 200, body: http_body)
expect(result).to eq %W[x\n z]
end
end
end
end
describe '#include_allowed?' do
context 'when allow-uri-read is nil' do
before do
allow(document).to receive(:attributes).and_return({ 'max-include-depth' => 100, 'allow-uri-read' => nil })
end
it 'allows http includes' do
expect(processor.send(:include_allowed?, 'http://example.com', reader)).to be_falsey
expect(processor.send(:include_allowed?, 'https://example.com', reader)).to be_falsey
end
it 'allows blob includes' do
expect(processor.send(:include_allowed?, 'a.blob', reader)).to be_truthy
end
end
context 'when allow-uri-read is false' do
before do
allow(document).to receive(:attributes).and_return({ 'max-include-depth' => 100, 'allow-uri-read' => false })
end
it 'allows http includes' do
expect(processor.send(:include_allowed?, 'http://example.com', reader)).to be_falsey
expect(processor.send(:include_allowed?, 'https://example.com', reader)).to be_falsey
end
it 'allows blob includes' do
expect(processor.send(:include_allowed?, 'a.blob', reader)).to be_truthy
end
end
context 'when allow-uri-read is true' do
before do
allow(document).to receive(:attributes).and_return({ 'max-include-depth' => 100, 'allow-uri-read' => true })
end
it 'allows http includes' do
expect(processor.send(:include_allowed?, 'http://example.com', reader)).to be_truthy
expect(processor.send(:include_allowed?, 'https://example.com', reader)).to be_truthy
end
it 'allows blob includes' do
expect(processor.send(:include_allowed?, 'a.blob', reader)).to be_truthy
end
end
context 'without allow-uri-read' do
before do
allow(document).to receive(:attributes).and_return({ 'max-include-depth' => 100 })
end
it 'forbids http includes' do
expect(processor.send(:include_allowed?, 'http://example.com', reader)).to be_falsey
expect(processor.send(:include_allowed?, 'https://example.com', reader)).to be_falsey
end
it 'allows blob includes' do
expect(processor.send(:include_allowed?, 'a.blob', reader)).to be_truthy
end
end
it 'allows the first include' do
expect(processor.send(:include_allowed?, 'foo.adoc', reader)).to be_truthy
end
it 'allows the Nth include' do
(max_includes - 1).times { processor.send(:read_blob, ref, 'a.adoc') }
(max_includes - 1).times { processor.send(:read_lines, 'a.adoc', nil) }
expect(processor.send(:include_allowed?, 'foo.adoc', reader)).to be_truthy
end
it 'disallows the Nth + 1 include' do
max_includes.times { processor.send(:read_blob, ref, 'a.adoc') }
max_includes.times { processor.send(:read_lines, 'a.adoc', nil) }
expect(processor.send(:include_allowed?, 'foo.adoc', reader)).to be_falsey
end

View File

@ -20,7 +20,7 @@ module Gitlab
expected_asciidoc_opts = {
safe: :secure,
backend: :gitlab_html5,
attributes: described_class::DEFAULT_ADOC_ATTRS.merge({ "kroki-server-url" => nil }),
attributes: described_class::DEFAULT_ADOC_ATTRS.merge({ "kroki-server-url" => nil, "allow-uri-read" => false }),
extensions: be_a(Proc)
}
@ -35,7 +35,7 @@ module Gitlab
expected_asciidoc_opts = {
safe: :secure,
backend: :gitlab_html5,
attributes: described_class::DEFAULT_ADOC_ATTRS.merge({ "kroki-server-url" => nil }),
attributes: described_class::DEFAULT_ADOC_ATTRS.merge({ "kroki-server-url" => nil, "allow-uri-read" => false }),
extensions: be_a(Proc)
}
@ -730,6 +730,19 @@ module Gitlab
include_examples 'invalid include'
end
context 'with a URI that returns 404' do
let(:include_path) { 'https://example.com/some_file.adoc' }
before do
stub_request(:get, include_path).to_return(status: 404, body: 'not found')
allow_any_instance_of(ApplicationSetting).to receive(:wiki_asciidoc_allow_uri_includes).and_return(true)
end
it 'renders Unresolved directive placeholder' do
is_expected.to include("<strong>[ERROR: include::#{include_path}[] - unresolved directive]</strong>")
end
end
context 'with path to a textual file' do
let(:include_path) { 'sample.adoc' }
@ -804,6 +817,59 @@ module Gitlab
end
end
describe 'the effect of max-includes' do
before do
create_file 'doc/preface.adoc', 'source: preface'
create_file 'doc/chapter-1.adoc', 'source: chapter-1'
create_file 'license.adoc', 'source: license'
stub_request(:get, 'https://example.com/some_file.adoc')
.to_return(status: 200, body: 'source: interwebs')
stub_request(:get, 'https://example.com/other_file.adoc')
.to_return(status: 200, body: 'source: intertubes')
allow_any_instance_of(ApplicationSetting).to receive(:wiki_asciidoc_allow_uri_includes).and_return(true)
end
let(:input) do
<<~ADOC
Source: requested file
include::doc/preface.adoc[]
include::https://example.com/some_file.adoc[]
include::doc/chapter-1.adoc[]
include::https://example.com/other_file.adoc[]
include::license.adoc[]
ADOC
end
it 'includes the content of all sources' do
expect(output.gsub(/<[^>]+>/, '').gsub(/\n\s*/, "\n").strip).to eq <<~ADOC.strip
Source: requested file
source: preface
source: interwebs
source: chapter-1
source: intertubes
source: license
ADOC
end
context 'when the document includes more than MAX_INCLUDES' do
before do
stub_const("#{described_class}::MAX_INCLUDES", 2)
end
it 'includes only the content of the first 2 sources' do
expect(output.gsub(/<[^>]+>/, '').gsub(/\n\s*/, "\n").strip).to eq <<~ADOC.strip
Source: requested file
source: preface
source: interwebs
doc/chapter-1.adoc
https://example.com/other_file.adoc
license.adoc
ADOC
end
end
end
context 'recursive includes with relative paths' do
let(:input) do
<<~ADOC
@ -811,29 +877,53 @@ module Gitlab
include::doc/README.adoc[]
include::license.adoc[]
include::https://example.com/some_file.adoc[]
include::license.adoc[lines=1]
ADOC
end
before do
stub_request(:get, 'https://example.com/some_file.adoc')
.to_return(status: 200, body: <<~ADOC)
Source: some file from Example.com
include::https://example.com/other_file[lines=1..2]
End some file from Example.com
ADOC
stub_request(:get, 'https://example.com/other_file')
.to_return(status: 200, body: <<~ADOC)
Source: other file from Example.com
Other file line 2
Other file line 3
ADOC
create_file 'doc/README.adoc', <<~ADOC
Source: doc/README.adoc
include::../license.adoc[]
include::../license.adoc[lines=1;3]
include::api/hello.adoc[]
ADOC
create_file 'license.adoc', <<~ADOC
Source: license.adoc
License content
License end
ADOC
create_file 'doc/api/hello.adoc', <<~ADOC
Source: doc/api/hello.adoc
include::./common.adoc[]
include::./common.adoc[lines=2..3]
ADOC
create_file 'doc/api/common.adoc', <<~ADOC
Common start
Source: doc/api/common.adoc
Common end
ADOC
allow_any_instance_of(ApplicationSetting).to receive(:wiki_asciidoc_allow_uri_includes).and_return(true)
end
it 'includes content of the included files recursively' do
@ -841,8 +931,14 @@ module Gitlab
Source: requested file
Source: doc/README.adoc
Source: license.adoc
License end
Source: doc/api/hello.adoc
Source: doc/api/common.adoc
Common end
Source: some file from Example.com
Source: other file from Example.com
Other file line 2
End some file from Example.com
Source: license.adoc
ADOC
end

View File

@ -131,6 +131,9 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
it { is_expected.to validate_numericality_of(:snippet_size_limit).only_integer.is_greater_than(0) }
it { is_expected.to validate_numericality_of(:wiki_page_max_content_bytes).only_integer.is_greater_than_or_equal_to(1024) }
it { is_expected.to allow_value(true).for(:wiki_asciidoc_allow_uri_includes) }
it { is_expected.to allow_value(false).for(:wiki_asciidoc_allow_uri_includes) }
it { is_expected.not_to allow_value(nil).for(:wiki_asciidoc_allow_uri_includes) }
it { is_expected.to validate_presence_of(:max_artifacts_size) }
it { is_expected.to validate_numericality_of(:max_artifacts_size).only_integer.is_greater_than(0) }
it { is_expected.to validate_presence_of(:max_yaml_size_bytes) }

View File

@ -46,6 +46,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
expect(json_response['spam_check_endpoint_url']).to be_nil
expect(json_response['spam_check_api_key']).to be_nil
expect(json_response['wiki_page_max_content_bytes']).to be_a(Integer)
expect(json_response['wiki_asciidoc_allow_uri_includes']).to be_falsey
expect(json_response['require_admin_approval_after_user_signup']).to eq(true)
expect(json_response['personal_access_token_prefix']).to eq('glpat-')
expect(json_response['admin_mode']).to be(false)
@ -166,6 +167,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
disabled_oauth_sign_in_sources: 'unknown',
import_sources: 'github,bitbucket',
wiki_page_max_content_bytes: 12345,
wiki_asciidoc_allow_uri_includes: true,
personal_access_token_prefix: "GL-",
user_deactivation_emails_enabled: false,
admin_mode: true,
@ -243,6 +245,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
expect(json_response['disabled_oauth_sign_in_sources']).to eq([])
expect(json_response['import_sources']).to match_array(%w(github bitbucket))
expect(json_response['wiki_page_max_content_bytes']).to eq(12345)
expect(json_response['wiki_asciidoc_allow_uri_includes']).to be(true)
expect(json_response['personal_access_token_prefix']).to eq("GL-")
expect(json_response['admin_mode']).to be(true)
expect(json_response['user_deactivation_emails_enabled']).to be(false)