diff --git a/CHANGELOG.md b/CHANGELOG.md index fd6150af203..b5f1235d615 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 13.8.2 (2021-02-01) + +### Security (5 changes) + +- Filter sensitive GraphQL variables from logs. +- Avoid exposing release links when the user cannot read git-tag/repository. +- Sanitize target branch on MR page. +- Fix DNS rebinding protection bypass when allowing an IP address in Outbound Requests setting. +- Add routes for unmatched url for not-get requests. + + ## 13.8.1 (2021-01-26) ### Fixed (3 changes) @@ -368,6 +379,17 @@ entry. - Add verbiage + link sast to show it's in core. !51935 +## 13.7.6 (2021-02-01) + +### Security (5 changes) + +- Filter sensitive GraphQL variables from logs. +- Avoid exposing release links when the user cannot read git-tag/repository. +- Sanitize target branch on MR page. +- Fix DNS rebinding protection bypass when allowing an IP address in Outbound Requests setting. +- Add routes for unmatched url for not-get requests. + + ## 13.7.5 (2021-01-25) ### Fixed (2 changes, 1 of them is from the community) @@ -878,6 +900,17 @@ entry. - Update GitLab Workhorse to v8.57.0. +## 13.6.6 (2021-02-01) + +### Security (5 changes) + +- Filter sensitive GraphQL variables from logs. +- Avoid exposing release links when the user cannot read git-tag/repository. +- Sanitize target branch on MR page. +- Fix DNS rebinding protection bypass when allowing an IP address in Outbound Requests setting. +- Add routes for unmatched url for not-get requests. + + ## 13.6.5 (2021-01-13) ### Security (1 change) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 541b10bb068..03c762b0ccd 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -9ed0124f6cdfc359521feae325420549781d883e +aeec1e34a8f0fc6b453b7f091e3712f17956b580 diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js index 75659bbf685..669bc90dcb9 100644 --- a/app/assets/javascripts/behaviors/index.js +++ b/app/assets/javascripts/behaviors/index.js @@ -25,19 +25,17 @@ initPageShortcuts(); initCollapseSidebarOnWindowResize(); initSelect2Dropdowns(); -document.addEventListener('DOMContentLoaded', () => { - window.requestIdleCallback( - () => { - // Check if we have to Load GFM Input - const $gfmInputs = $('.js-gfm-input:not(.js-gfm-input-initialized)'); - if ($gfmInputs.length) { - import(/* webpackChunkName: 'initGFMInput' */ './markdown/gfm_auto_complete') - .then(({ default: initGFMInput }) => { - initGFMInput($gfmInputs); - }) - .catch(() => {}); - } - }, - { timeout: 500 }, - ); -}); +window.requestIdleCallback( + () => { + // Check if we have to Load GFM Input + const $gfmInputs = $('.js-gfm-input:not(.js-gfm-input-initialized)'); + if ($gfmInputs.length) { + import(/* webpackChunkName: 'initGFMInput' */ './markdown/gfm_auto_complete') + .then(({ default: initGFMInput }) => { + initGFMInput($gfmInputs); + }) + .catch(() => {}); + } + }, + { timeout: 500 }, +); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue index a863976138c..2bf86c1863a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue @@ -1,5 +1,6 @@ ', + }, + }); + + expect(wrapper.find(MrWidgetPipeline).props().sourceBranchLink).toBe('Foo'); + }); + it('renders deployments', () => { const expectedProps = mockStore.postMergeDeployments.map((dep) => expect.objectContaining({ diff --git a/spec/lib/gitlab/composer/cache_spec.rb b/spec/lib/gitlab/composer/cache_spec.rb new file mode 100644 index 00000000000..00318ac14f9 --- /dev/null +++ b/spec/lib/gitlab/composer/cache_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Composer::Cache do + let_it_be(:package_name) { 'sample-project' } + let_it_be(:json) { { 'name' => package_name } } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :custom_repo, files: { 'composer.json' => json.to_json }, group: group) } + let(:branch) { project.repository.find_branch('master') } + let(:sha_regex) { /^[A-Fa-f0-9]{64}$/ } + + shared_examples 'Composer create cache page' do + let(:expected_json) { ::Gitlab::Composer::VersionIndex.new(packages).to_json } + + before do + stub_composer_cache_object_storage + end + + it 'creates the cached page' do + expect { subject }.to change { Packages::Composer::CacheFile.count }.by(1) + cache_file = Packages::Composer::CacheFile.last + expect(cache_file.file_sha256).to eq package.reload.composer_metadatum.version_cache_sha + expect(cache_file.file.read).to eq expected_json + end + end + + shared_examples 'Composer marks cache page for deletion' do + it 'marks the page for deletion' do + cache_file = Packages::Composer::CacheFile.last + + freeze_time do + expect { subject }.to change { cache_file.reload.delete_at}.from(nil).to(1.day.from_now) + end + end + end + + describe '#execute' do + subject { described_class.new(project: project, name: package_name).execute } + + context 'creating packages' do + context 'with a pre-existing package' do + let(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) } + let(:package2) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '2.0.0', json: json) } + let(:packages) { [package, package2] } + + before do + package + described_class.new(project: project, name: package_name).execute + package.reload + package2 + end + + it 'updates the sha and creates the cache page' do + expect { subject }.to change { package2.reload.composer_metadatum.version_cache_sha }.from(nil).to(sha_regex) + .and change { package.reload.composer_metadatum.version_cache_sha }.to(sha_regex) + end + + it_behaves_like 'Composer create cache page' + it_behaves_like 'Composer marks cache page for deletion' + end + + context 'first package' do + let!(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) } + let(:packages) { [package] } + + it 'updates the sha and creates the cache page' do + expect { subject }.to change { package.reload.composer_metadatum.version_cache_sha }.from(nil).to(sha_regex) + end + + it_behaves_like 'Composer create cache page' + end + end + + context 'updating packages' do + let(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) } + let(:package2) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '2.0.0', json: json) } + let(:packages) { [package, package2] } + + before do + packages + + described_class.new(project: project, name: package_name).execute + + package.update!(version: '1.2.0') + package.reload + end + + it_behaves_like 'Composer create cache page' + it_behaves_like 'Composer marks cache page for deletion' + end + + context 'deleting packages' do + context 'when it is not the last package' do + let(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) } + let(:package2) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '2.0.0', json: json) } + let(:packages) { [package] } + + before do + package + package2 + + described_class.new(project: project, name: package_name).execute + + package2.destroy! + end + + it_behaves_like 'Composer create cache page' + it_behaves_like 'Composer marks cache page for deletion' + end + + context 'when it is the last package' do + let!(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) } + let!(:last_sha) do + described_class.new(project: project, name: package_name).execute + package.reload.composer_metadatum.version_cache_sha + end + + before do + package.destroy! + end + + subject { described_class.new(project: project, name: package_name, last_page_sha: last_sha).execute } + + it_behaves_like 'Composer marks cache page for deletion' + + it 'does not create a new page' do + expect { subject }.not_to change { Packages::Composer::CacheFile.count } + end + end + end + end +end diff --git a/spec/lib/gitlab/composer/version_index_spec.rb b/spec/lib/gitlab/composer/version_index_spec.rb index 4c4742d9f59..7b0ed703f42 100644 --- a/spec/lib/gitlab/composer/version_index_spec.rb +++ b/spec/lib/gitlab/composer/version_index_spec.rb @@ -15,7 +15,9 @@ RSpec.describe Gitlab::Composer::VersionIndex do let(:packages) { [package1, package2] } describe '#as_json' do - subject(:index) { described_class.new(packages).as_json } + subject(:package_index) { index['packages'][package_name] } + + let(:index) { described_class.new(packages).as_json } def expected_json(package) { @@ -32,10 +34,16 @@ RSpec.describe Gitlab::Composer::VersionIndex do end it 'returns the packages json' do - packages = index['packages'][package_name] + expect(package_index['1.0.0']).to eq(expected_json(package1)) + expect(package_index['2.0.0']).to eq(expected_json(package2)) + end - expect(packages['1.0.0']).to eq(expected_json(package1)) - expect(packages['2.0.0']).to eq(expected_json(package2)) + context 'with an unordered list of packages' do + let(:packages) { [package2, package1] } + + it 'returns the packages sorted by version' do + expect(package_index.keys).to eq ['1.0.0', '2.0.0'] + end end end diff --git a/spec/lib/gitlab/crypto_helper_spec.rb b/spec/lib/gitlab/crypto_helper_spec.rb index c07089d8ef0..024564ea213 100644 --- a/spec/lib/gitlab/crypto_helper_spec.rb +++ b/spec/lib/gitlab/crypto_helper_spec.rb @@ -19,21 +19,85 @@ RSpec.describe Gitlab::CryptoHelper do expect(encrypted).to match %r{\A[A-Za-z0-9+/=]+\z} expect(encrypted).not_to include "\n" end + + it 'does not save hashed token with iv value in database' do + expect { described_class.aes256_gcm_encrypt('some-value') }.not_to change { TokenWithIv.count } + end + + it 'encrypts using static iv' do + expect(Encryptor).to receive(:encrypt).with(described_class::AES256_GCM_OPTIONS.merge(value: 'some-value', iv: described_class::AES256_GCM_IV_STATIC)).and_return('hashed_value') + + described_class.aes256_gcm_encrypt('some-value') + end end describe '.aes256_gcm_decrypt' do - let(:encrypted) { described_class.aes256_gcm_encrypt('some-value') } - - it 'correctly decrypts encrypted string' do - decrypted = described_class.aes256_gcm_decrypt(encrypted) - - expect(decrypted).to eq 'some-value' + before do + stub_feature_flags(dynamic_nonce_creation: false) end - it 'decrypts a value when it ends with a new line character' do - decrypted = described_class.aes256_gcm_decrypt(encrypted + "\n") + context 'when token was encrypted using static nonce' do + let(:encrypted) { described_class.aes256_gcm_encrypt('some-value', nonce: described_class::AES256_GCM_IV_STATIC) } - expect(decrypted).to eq 'some-value' + it 'correctly decrypts encrypted string' do + decrypted = described_class.aes256_gcm_decrypt(encrypted) + + expect(decrypted).to eq 'some-value' + end + + it 'decrypts a value when it ends with a new line character' do + decrypted = described_class.aes256_gcm_decrypt(encrypted + "\n") + + expect(decrypted).to eq 'some-value' + end + + it 'does not save hashed token with iv value in database' do + expect { described_class.aes256_gcm_decrypt(encrypted) }.not_to change { TokenWithIv.count } + end + + context 'with feature flag switched on' do + before do + stub_feature_flags(dynamic_nonce_creation: true) + end + + it 'correctly decrypts encrypted string' do + decrypted = described_class.aes256_gcm_decrypt(encrypted) + + expect(decrypted).to eq 'some-value' + end + end + end + + context 'when token was encrypted using random nonce' do + let(:value) { 'random-value' } + + # for compatibility with tokens encrypted using dynamic nonce + let!(:encrypted) do + iv = create_nonce + encrypted_token = described_class.create_encrypted_token(value, iv) + TokenWithIv.create!(hashed_token: Digest::SHA256.digest(encrypted_token), hashed_plaintext_token: Digest::SHA256.digest(encrypted_token), iv: iv) + encrypted_token + end + + before do + stub_feature_flags(dynamic_nonce_creation: true) + end + + it 'correctly decrypts encrypted string' do + decrypted = described_class.aes256_gcm_decrypt(encrypted) + + expect(decrypted).to eq value + end + + it 'does not save hashed token with iv value in database' do + expect { described_class.aes256_gcm_decrypt(encrypted) }.not_to change { TokenWithIv.count } + end end end + + def create_nonce + cipher = OpenSSL::Cipher.new('aes-256-gcm') + cipher.encrypt # Required before '#random_iv' can be called + cipher.random_iv # Ensures that the IV is the correct length respective to the algorithm used. + end end diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb index 786db23ffc4..01aceec12c5 100644 --- a/spec/lib/gitlab/current_settings_spec.rb +++ b/spec/lib/gitlab/current_settings_spec.rb @@ -194,4 +194,32 @@ RSpec.describe Gitlab::CurrentSettings do end end end + + describe '#current_application_settings?', :use_clean_rails_memory_store_caching do + before do + allow(Gitlab::CurrentSettings).to receive(:current_application_settings?).and_call_original + end + + it 'returns true when settings exist' do + create(:application_setting, + home_page_url: 'http://mydomain.com', + signup_enabled: false) + + expect(described_class.current_application_settings?).to eq(true) + end + + it 'returns false when settings do not exist' do + expect(described_class.current_application_settings?).to eq(false) + end + + context 'with cache', :request_store do + include_context 'with settings in cache' + + it 'returns an in-memory ApplicationSetting object' do + expect(ApplicationSetting).not_to receive(:current) + + expect(described_class.current_application_settings?).to eq(true) + end + end + end end diff --git a/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb b/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb index c8432513185..138765afd8a 100644 --- a/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb +++ b/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb @@ -40,4 +40,22 @@ RSpec.describe Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer do end end end + + describe '#initial_value' do + it 'filters out sensitive variables' do + doc = GraphQL.parse <<-GRAPHQL + mutation createNote($body: String!) { + createNote(input: {noteableId: "1", body: $body}) { + note { + id + } + } + } + GRAPHQL + + query = GraphQL::Query.new(GitlabSchema, document: doc, context: {}, variables: { body: "some note" }) + + expect(subject.initial_value(query)[:variables]).to eq('{:body=>"[FILTERED]"}') + end + end end diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb index f466d117851..686382dc262 100644 --- a/spec/lib/gitlab/url_blocker_spec.rb +++ b/spec/lib/gitlab/url_blocker_spec.rb @@ -91,6 +91,21 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do end end + context 'DNS rebinding protection with IP allowed' do + let(:import_url) { 'http://a.192.168.0.120.3times.127.0.0.1.1time.repeat.rebind.network:9121/scrape?target=unix:///var/opt/gitlab/redis/redis.socket&check-keys=*' } + + before do + stub_dns(import_url, ip_address: '192.168.0.120') + + allow(Gitlab::UrlBlockers::UrlAllowlist).to receive(:ip_allowed?).and_return(true) + end + + it_behaves_like 'validates URI and hostname' do + let(:expected_uri) { 'http://192.168.0.120:9121/scrape?target=unix:///var/opt/gitlab/redis/redis.socket&check-keys=*' } + let(:expected_hostname) { 'a.192.168.0.120.3times.127.0.0.1.1time.repeat.rebind.network' } + end + end + context 'disabled DNS rebinding protection' do subject { described_class.validate!(import_url, dns_rebind_protection: false) } diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb index e1b8323eb8e..5f945d5b9fc 100644 --- a/spec/lib/gitlab_spec.rb +++ b/spec/lib/gitlab_spec.rb @@ -95,6 +95,26 @@ RSpec.describe Gitlab do end end + describe '.com' do + subject { described_class.com { true } } + + before do + allow(described_class).to receive(:com?).and_return(gl_com) + end + + context 'when on GitLab.com' do + let(:gl_com) { true } + + it { is_expected.to be true } + end + + context 'when not on GitLab.com' do + let(:gl_com) { false } + + it { is_expected.to be_nil } + end + end + describe '.staging?' do subject { described_class.staging? } @@ -332,13 +352,13 @@ RSpec.describe Gitlab do describe '.maintenance_mode?' do it 'returns true when maintenance mode is enabled' do - stub_application_setting(maintenance_mode: true) + stub_maintenance_mode_setting(true) expect(described_class.maintenance_mode?).to eq(true) end it 'returns false when maintenance mode is disabled' do - stub_application_setting(maintenance_mode: false) + stub_maintenance_mode_setting(false) expect(described_class.maintenance_mode?).to eq(false) end diff --git a/spec/migrations/encrypt_feature_flags_clients_tokens_spec.rb b/spec/migrations/encrypt_feature_flags_clients_tokens_spec.rb index ad83119f324..c705515ce98 100644 --- a/spec/migrations/encrypt_feature_flags_clients_tokens_spec.rb +++ b/spec/migrations/encrypt_feature_flags_clients_tokens_spec.rb @@ -8,7 +8,7 @@ RSpec.describe EncryptFeatureFlagsClientsTokens do let(:feature_flags_clients) { table(:operations_feature_flags_clients) } let(:projects) { table(:projects) } let(:plaintext) { "secret-token" } - let(:ciphertext) { Gitlab::CryptoHelper.aes256_gcm_encrypt(plaintext) } + let(:ciphertext) { Gitlab::CryptoHelper.aes256_gcm_encrypt(plaintext, nonce: Gitlab::CryptoHelper::AES256_GCM_IV_STATIC) } describe '#up' do it 'keeps plaintext token the same and populates token_encrypted if not present' do diff --git a/spec/models/active_session_spec.rb b/spec/models/active_session_spec.rb index f0bae3f29c0..51435cc4342 100644 --- a/spec/models/active_session_spec.rb +++ b/spec/models/active_session_spec.rb @@ -358,7 +358,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do it 'calls .destroy_sessions' do expect(ActiveSession).to( receive(:destroy_sessions) - .with(anything, user, [active_session.public_id, rack_session.public_id, rack_session.private_id])) + .with(anything, user, [encrypted_active_session_id, rack_session.public_id, rack_session.private_id])) subject end diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index d8b77e1cd0d..2df76684d71 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -54,7 +54,7 @@ RSpec.describe ApplicationSetting, 'TokenAuthenticatable' do it 'persists new token as an encrypted string' do expect(subject).to eq settings.reload.runners_registration_token expect(settings.read_attribute('runners_registration_token_encrypted')) - .to eq Gitlab::CryptoHelper.aes256_gcm_encrypt(subject) + .to eq Gitlab::CryptoHelper.aes256_gcm_encrypt(subject, nonce: Gitlab::CryptoHelper::AES256_GCM_IV_STATIC) expect(settings).to be_persisted end @@ -243,7 +243,7 @@ RSpec.describe Ci::Build, 'TokenAuthenticatable' do it 'persists new token as an encrypted string' do build.ensure_token! - encrypted = Gitlab::CryptoHelper.aes256_gcm_encrypt(build.token) + encrypted = Gitlab::CryptoHelper.aes256_gcm_encrypt(build.token, nonce: Gitlab::CryptoHelper::AES256_GCM_IV_STATIC) expect(build.read_attribute('token_encrypted')).to eq encrypted end diff --git a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb index f6b8cf7def4..1e1cd97e410 100644 --- a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb +++ b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb @@ -68,6 +68,10 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do context 'when using optional strategy' do let(:options) { { encrypted: :optional } } + before do + stub_feature_flags(dynamic_nonce_creation: false) + end + it 'returns decrypted token when an encrypted token is present' do allow(instance).to receive(:read_attribute) .with('some_field_encrypted') @@ -124,7 +128,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do it 'writes encrypted token and removes plaintext token and returns it' do expect(instance).to receive(:[]=) - .with('some_field_encrypted', encrypted) + .with('some_field_encrypted', any_args) expect(instance).to receive(:[]=) .with('some_field', nil) @@ -137,7 +141,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do it 'writes encrypted token and writes plaintext token' do expect(instance).to receive(:[]=) - .with('some_field_encrypted', encrypted) + .with('some_field_encrypted', any_args) expect(instance).to receive(:[]=) .with('some_field', 'my-value') diff --git a/spec/models/packages/composer/cache_file_spec.rb b/spec/models/packages/composer/cache_file_spec.rb new file mode 100644 index 00000000000..ef9818f0930 --- /dev/null +++ b/spec/models/packages/composer/cache_file_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Composer::CacheFile, type: :model do + describe 'relationships' do + it { is_expected.to belong_to(:group) } + it { is_expected.to belong_to(:namespace) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:namespace) } + end + + describe 'scopes' do + let_it_be(:group1) { create(:group) } + let_it_be(:group2) { create(:group) } + let_it_be(:cache_file1) { create(:composer_cache_file, file_sha256: '123456', group: group1) } + let_it_be(:cache_file2) { create(:composer_cache_file, file_sha256: '456778', group: group2) } + + describe '.with_namespace' do + subject { described_class.with_namespace(group1) } + + it { is_expected.to eq [cache_file1] } + end + + describe '.with_sha' do + subject { described_class.with_sha('123456') } + + it { is_expected.to eq [cache_file1] } + end + end +end diff --git a/spec/models/packages/composer/metadatum_spec.rb b/spec/models/packages/composer/metadatum_spec.rb index ae53532696b..1c888f1563c 100644 --- a/spec/models/packages/composer/metadatum_spec.rb +++ b/spec/models/packages/composer/metadatum_spec.rb @@ -11,4 +11,20 @@ RSpec.describe Packages::Composer::Metadatum, type: :model do it { is_expected.to validate_presence_of(:target_sha) } it { is_expected.to validate_presence_of(:composer_json) } end + + describe 'scopes' do + let_it_be(:package_name) { 'sample-project' } + let_it_be(:json) { { 'name' => package_name } } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :custom_repo, files: { 'composer.json' => json.to_json }, group: group) } + let_it_be(:package1) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) } + let_it_be(:package2) { create(:composer_package, :with_metadatum, project: project, name: 'other-name', version: '1.0.0', json: json) } + let_it_be(:package3) { create(:pypi_package, name: package_name, project: project) } + + describe '.for_package' do + subject { described_class.for_package(package_name, project.id) } + + it { is_expected.to eq [package1.composer_metadatum] } + end + end end diff --git a/spec/models/token_with_iv_spec.rb b/spec/models/token_with_iv_spec.rb new file mode 100644 index 00000000000..8dbccc19217 --- /dev/null +++ b/spec/models/token_with_iv_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe TokenWithIv do + describe 'validations' do + it { is_expected.to validate_presence_of :hashed_token } + it { is_expected.to validate_presence_of :iv } + it { is_expected.to validate_presence_of :hashed_plaintext_token } + end + + describe '.find_by_hashed_token' do + it 'only includes matching record' do + matching_record = create(:token_with_iv, hashed_token: ::Digest::SHA256.digest('hashed-token')) + create(:token_with_iv) + + expect(described_class.find_by_hashed_token('hashed-token')).to eq(matching_record) + end + end + + describe '.find_by_plaintext_token' do + it 'only includes matching record' do + matching_record = create(:token_with_iv, hashed_plaintext_token: ::Digest::SHA256.digest('hashed-token')) + create(:token_with_iv) + + expect(described_class.find_by_plaintext_token('hashed-token')).to eq(matching_record) + end + end +end diff --git a/spec/presenters/release_presenter_spec.rb b/spec/presenters/release_presenter_spec.rb index b518584569b..4bf12183eff 100644 --- a/spec/presenters/release_presenter_spec.rb +++ b/spec/presenters/release_presenter_spec.rb @@ -62,6 +62,12 @@ RSpec.describe ReleasePresenter do it 'returns its own url' do is_expected.to eq(project_release_url(project, release)) end + + context 'when user is guest' do + let(:user) { guest } + + it { is_expected.to be_nil } + end end describe '#opened_merge_requests_url' do diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index bc89dc2fa77..1ee3e36be8b 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -159,13 +159,17 @@ RSpec.describe 'Git HTTP requests' do context "POST git-upload-pack" do it "fails to find a route" do - expect { clone_post(repository_path) }.to raise_error(ActionController::RoutingError) + clone_post(repository_path) do |response| + expect(response).to have_gitlab_http_status(:not_found) + end end end context "POST git-receive-pack" do it "fails to find a route" do - expect { push_post(repository_path) }.to raise_error(ActionController::RoutingError) + push_post(repository_path) do |response| + expect(response).to have_gitlab_http_status(:not_found) + end end end end diff --git a/spec/routing/git_http_routing_spec.rb b/spec/routing/git_http_routing_spec.rb index e3cc1440a9e..79d392e4132 100644 --- a/spec/routing/git_http_routing_spec.rb +++ b/spec/routing/git_http_routing_spec.rb @@ -7,6 +7,10 @@ RSpec.describe 'git_http routing' do it_behaves_like 'git repository routes' do let(:path) { '/gitlab-org/gitlab-test.git' } end + + it_behaves_like 'git repository routes with fallback for git-upload-pack' do + let(:path) { '/gitlab-org/gitlab-test.git' } + end end describe 'wiki repositories' do @@ -14,6 +18,7 @@ RSpec.describe 'git_http routing' do let(:path) { '/gitlab-org/gitlab-test.wiki.git' } it_behaves_like 'git repository routes' + it_behaves_like 'git repository routes with fallback for git-upload-pack' describe 'redirects', type: :request do let(:web_path) { '/gitlab-org/gitlab-test/-/wikis' } @@ -37,12 +42,20 @@ RSpec.describe 'git_http routing' do it_behaves_like 'git repository routes' do let(:path) { '/gitlab-org.wiki.git' } end + + it_behaves_like 'git repository routes with fallback for git-upload-pack' do + let(:path) { '/gitlab-org.wiki.git' } + end end context 'in child group' do it_behaves_like 'git repository routes' do let(:path) { '/gitlab-org/child.wiki.git' } end + + it_behaves_like 'git repository routes with fallback for git-upload-pack' do + let(:path) { '/gitlab-org/child.wiki.git' } + end end end @@ -51,12 +64,20 @@ RSpec.describe 'git_http routing' do it_behaves_like 'git repository routes' do let(:path) { '/snippets/123.git' } end + + it_behaves_like 'git repository routes without fallback' do + let(:path) { '/snippets/123.git' } + end end context 'project snippet' do it_behaves_like 'git repository routes' do let(:path) { '/gitlab-org/gitlab-test/snippets/123.git' } end + + it_behaves_like 'git repository routes with fallback' do + let(:path) { '/gitlab-org/gitlab-test/snippets/123.git' } + end end end end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 29e5c1b4bae..f7ed8d7d5dc 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -876,4 +876,73 @@ RSpec.describe 'project routing' do ) end end + + context 'with a non-existent project' do + it 'routes to 404 with get request' do + expect(get: "/gitlab/not_exist").to route_to( + 'application#route_not_found', + unmatched_route: 'gitlab/not_exist' + ) + end + + it 'routes to 404 with delete request' do + expect(delete: "/gitlab/not_exist").to route_to( + 'application#route_not_found', + namespace_id: 'gitlab', + project_id: 'not_exist' + ) + end + + it 'routes to 404 with post request' do + expect(post: "/gitlab/not_exist").to route_to( + 'application#route_not_found', + namespace_id: 'gitlab', + project_id: 'not_exist' + ) + end + + it 'routes to 404 with put request' do + expect(put: "/gitlab/not_exist").to route_to( + 'application#route_not_found', + namespace_id: 'gitlab', + project_id: 'not_exist' + ) + end + + context 'with route to some action' do + it 'routes to 404 with get request to' do + expect(get: "/gitlab/not_exist/some_action").to route_to( + 'application#route_not_found', + unmatched_route: 'gitlab/not_exist/some_action' + ) + end + + it 'routes to 404 with delete request' do + expect(delete: "/gitlab/not_exist/some_action").to route_to( + 'application#route_not_found', + namespace_id: 'gitlab', + project_id: 'not_exist', + all: 'some_action' + ) + end + + it 'routes to 404 with post request' do + expect(post: "/gitlab/not_exist/some_action").to route_to( + 'application#route_not_found', + namespace_id: 'gitlab', + project_id: 'not_exist', + all: 'some_action' + ) + end + + it 'routes to 404 with put request' do + expect(put: "/gitlab/not_exist/some_action").to route_to( + 'application#route_not_found', + namespace_id: 'gitlab', + project_id: 'not_exist', + all: 'some_action' + ) + end + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b986777b361..b1b106f58ff 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -284,6 +284,8 @@ RSpec.configure do |config| current_user_mode.send(:user)&.admin? end end + + allow(Gitlab::CurrentSettings).to receive(:current_application_settings?).and_return(false) end config.around(:example, :quarantine) do |example| diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb index 3b733a2e57a..9851a3de9e9 100644 --- a/spec/support/helpers/stub_configuration.rb +++ b/spec/support/helpers/stub_configuration.rb @@ -121,6 +121,12 @@ module StubConfiguration allow(::Gitlab.config.packages).to receive_messages(to_settings(messages)) end + def stub_maintenance_mode_setting(value) + allow(Gitlab::CurrentSettings).to receive(:current_application_settings?).and_return(true) + + stub_application_setting(maintenance_mode: value) + end + private # Modifies stubbed messages to also stub possible predicate versions diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb index dc54a21d0fa..0d0ac171baa 100644 --- a/spec/support/helpers/stub_object_storage.rb +++ b/spec/support/helpers/stub_object_storage.rb @@ -85,6 +85,13 @@ module StubObjectStorage **params) end + def stub_composer_cache_object_storage(**params) + stub_object_storage_uploader(config: Gitlab.config.packages.object_store, + uploader: ::Packages::Composer::CacheUploader, + remote_directory: 'packages', + **params) + end + def stub_uploads_object_storage(uploader = described_class, **params) stub_object_storage_uploader(config: Gitlab.config.uploads.object_store, uploader: uploader, diff --git a/spec/support/matchers/route_to_route_not_found_matcher.rb b/spec/support/matchers/route_to_route_not_found_matcher.rb new file mode 100644 index 00000000000..4105f0f9191 --- /dev/null +++ b/spec/support/matchers/route_to_route_not_found_matcher.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +RSpec::Matchers.define :route_to_route_not_found do + match do |actual| + expect(actual).to route_to(controller: 'application', action: 'route_not_found') + rescue RSpec::Expectations::ExpectationNotMetError => e + # `route_to` matcher requires providing all params for exact match. As we use it in shared examples and we provide different paths, + # this matcher checks if provided route matches controller and action, without checking params. + expect(e.message).to include("-{\"controller\"=>\"application\", \"action\"=>\"route_not_found\"}\n+{\"controller\"=>\"application\", \"action\"=>\"route_not_found\",") + end + + failure_message do |_| + "expected #{actual} to route to route_not_found" + end +end diff --git a/spec/support/shared_examples/routing/git_http_routing_shared_examples.rb b/spec/support/shared_examples/routing/git_http_routing_shared_examples.rb index b0e1e942d81..f924da37f4f 100644 --- a/spec/support/shared_examples/routing/git_http_routing_shared_examples.rb +++ b/spec/support/shared_examples/routing/git_http_routing_shared_examples.rb @@ -16,10 +16,6 @@ RSpec.shared_examples 'git repository routes' do expect(get("#{container_path}/info/refs?service=git-upload-pack")).to redirect_to("#{container_path}.git/info/refs?service=git-upload-pack") expect(get("#{container_path}/info/refs?service=git-receive-pack")).to redirect_to("#{container_path}.git/info/refs?service=git-receive-pack") end - - it 'does not redirect other requests' do - expect(post("#{container_path}/git-upload-pack")).not_to be_routable - end end it 'routes LFS endpoints' do @@ -35,6 +31,56 @@ RSpec.shared_examples 'git repository routes' do expect(get("#{path}/gitlab-lfs/objects/#{oid}")).to route_to('repositories/lfs_storage#download', oid: oid, **params) expect(put("#{path}/gitlab-lfs/objects/#{oid}/456/authorize")).to route_to('repositories/lfs_storage#upload_authorize', oid: oid, size: '456', **params) expect(put("#{path}/gitlab-lfs/objects/#{oid}/456")).to route_to('repositories/lfs_storage#upload_finalize', oid: oid, size: '456', **params) + end +end + +RSpec.shared_examples 'git repository routes without fallback' do + let(:container_path) { path.delete_suffix('.git') } + + context 'requests without .git format' do + it 'does not redirect other requests' do + expect(post("#{container_path}/git-upload-pack")).not_to be_routable + end + end + + it 'routes LFS endpoints for unmatched routes' do + oid = generate(:oid) + + expect(put("#{path}/gitlab-lfs/objects/foo")).not_to be_routable + expect(put("#{path}/gitlab-lfs/objects/#{oid}/foo")).not_to be_routable + expect(put("#{path}/gitlab-lfs/objects/#{oid}/foo/authorize")).not_to be_routable + end +end + +RSpec.shared_examples 'git repository routes with fallback' do + let(:container_path) { path.delete_suffix('.git') } + + context 'requests without .git format' do + it 'does not redirect other requests' do + expect(post("#{container_path}/git-upload-pack")).to route_to_route_not_found + end + end + + it 'routes LFS endpoints' do + oid = generate(:oid) + + expect(put("#{path}/gitlab-lfs/objects/foo")).to route_to_route_not_found + expect(put("#{path}/gitlab-lfs/objects/#{oid}/foo")).to route_to_route_not_found + expect(put("#{path}/gitlab-lfs/objects/#{oid}/foo/authorize")).to route_to_route_not_found + end +end + +RSpec.shared_examples 'git repository routes with fallback for git-upload-pack' do + let(:container_path) { path.delete_suffix('.git') } + + context 'requests without .git format' do + it 'does not redirect other requests' do + expect(post("#{container_path}/git-upload-pack")).to route_to_route_not_found + end + end + + it 'routes LFS endpoints for unmatched routes' do + oid = generate(:oid) expect(put("#{path}/gitlab-lfs/objects/foo")).not_to be_routable expect(put("#{path}/gitlab-lfs/objects/#{oid}/foo")).not_to be_routable diff --git a/spec/uploaders/packages/composer/cache_uploader_spec.rb b/spec/uploaders/packages/composer/cache_uploader_spec.rb new file mode 100644 index 00000000000..a4ba4cc2a1e --- /dev/null +++ b/spec/uploaders/packages/composer/cache_uploader_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Composer::CacheUploader do + let(:cache_file) { create(:composer_cache_file) } # rubocop:disable Rails/SaveBang + let(:uploader) { described_class.new(cache_file, :file) } + let(:path) { Gitlab.config.packages.storage_path } + + subject { uploader } + + it_behaves_like "builds correct paths", + store_dir: %r[^\h{2}/\h{2}/\h{64}/packages/composer_cache/\d+$], + cache_dir: %r[/packages/tmp/cache], + work_dir: %r[/packages/tmp/work] + + context 'object store is remote' do + before do + stub_composer_cache_object_storage + end + + include_context 'with storage', described_class::Store::REMOTE + + it_behaves_like "builds correct paths", + store_dir: %r[^\h{2}/\h{2}/\h{64}/packages/composer_cache/\d+$] + end + + describe 'remote file' do + let(:cache_file) { create(:composer_cache_file, :object_storage) } + + context 'with object storage enabled' do + before do + stub_composer_cache_object_storage + end + + it 'can store file remotely' do + allow(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async) + + cache_file + + expect(cache_file.file_store).to eq(described_class::Store::REMOTE) + expect(cache_file.file.path).not_to be_blank + end + end + end +end