Commit 8130e589 authored by Stan Hu's avatar Stan Hu

Merge branch '288827-feature-flag-rollout-of-diff_check_with_paths_changed_rpc' into 'master'

Resolve "[Feature flag] Rollout of `diff_check_with_paths_changed_rpc`"

See merge request gitlab-org/gitlab!50623
parents 130de313 de990d4b
---
name: diff_check_with_paths_changed_rpc
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46116
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/288827
milestone: '13.7'
type: development
group: group::code review
default_enabled: true
...@@ -9,7 +9,7 @@ module EE ...@@ -9,7 +9,7 @@ module EE
private private
def path_validations def file_paths_validations
validations = [super].flatten validations = [super].flatten
if validate_code_owners? if validate_code_owners?
...@@ -48,8 +48,8 @@ module EE ...@@ -48,8 +48,8 @@ module EE
push_rule.file_name_regex.present? || push_rule.prevent_secrets push_rule.file_name_regex.present? || push_rule.prevent_secrets
end end
override :validations_for_diff override :validations_for_path
def validations_for_diff def validations_for_path
super.tap do |validations| super.tap do |validations|
validations.push(path_locks_validation) if validate_path_locks? validations.push(path_locks_validation) if validate_path_locks?
validations.push(file_name_validation) if push_rule_checks_commit? validations.push(file_name_validation) if push_rule_checks_commit?
...@@ -57,17 +57,8 @@ module EE ...@@ -57,17 +57,8 @@ module EE
end end
def path_locks_validation def path_locks_validation
lambda do |diff| lambda do |changed_path|
path = if ::Feature.enabled?(:diff_check_with_paths_changed_rpc, project, default_enabled: true) path = changed_path.path
diff.path
else
if diff.renamed_file?
diff.old_path
else
diff.new_path || diff.old_path
end
end
lock_info = project.find_path_lock(path) lock_info = project.find_path_lock(path)
if lock_info && lock_info.user != user_access.user if lock_info && lock_info.user != user_access.user
...@@ -76,24 +67,12 @@ module EE ...@@ -76,24 +67,12 @@ module EE
end end
end end
def new_file?(path)
path.status == :ADDED
end
def file_name_validation def file_name_validation
lambda do |diff| lambda do |changed_path|
if ::Feature.enabled?(:diff_check_with_paths_changed_rpc, project, default_enabled: true) if changed_path.new_file? && denylisted_regex = push_rule.filename_denylisted?(changed_path.path)
if new_file?(diff) && denylisted_regex = push_rule.filename_denylisted?(diff.path) return unless denylisted_regex.present?
return unless denylisted_regex.present?
"File name #{changed_path.path} was blacklisted by the pattern #{denylisted_regex}."
"File name #{diff.path} was blacklisted by the pattern #{denylisted_regex}."
end
else
if (diff.renamed_file || diff.new_file) && denylisted_regex = push_rule.filename_denylisted?(diff.new_path)
return unless denylisted_regex.present?
"File name #{diff.new_path} was blacklisted by the pattern #{denylisted_regex}."
end
end end
rescue ::PushRule::MatchError => e rescue ::PushRule::MatchError => e
raise ::Gitlab::GitAccess::ForbiddenError, e.message raise ::Gitlab::GitAccess::ForbiddenError, e.message
......
...@@ -6,37 +6,20 @@ module Gitlab ...@@ -6,37 +6,20 @@ module Gitlab
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
LOG_MESSAGES = { LOG_MESSAGES = {
validate_file_paths: "Validating diffs' file paths...", validate_file_paths: "Validating diffs' file paths..."
diff_content_check: "Validating diff contents..."
}.freeze }.freeze
def validate! def validate!
return if deletion? return if deletion?
return unless should_run_diff_validations? return unless should_run_validations?
return if commits.empty? return if commits.empty?
file_paths = [] paths = project.repository.find_changed_paths(commits.map(&:sha))
paths.each do |path|
if ::Feature.enabled?(:diff_check_with_paths_changed_rpc, project, default_enabled: true) validate_path(path)
paths = project.repository.find_changed_paths(commits.map(&:sha))
paths.each do |path|
file_paths.concat([path.path])
validate_diff(path)
end
else
process_commits do |commit|
validate_once(commit) do
commit.raw_deltas.each do |diff|
file_paths.concat([diff.new_path, diff.old_path].compact)
validate_diff(diff)
end
end
end
end end
validate_file_paths(file_paths.uniq) validate_file_paths(paths.map(&:path).uniq)
end end
private private
...@@ -47,43 +30,30 @@ module Gitlab ...@@ -47,43 +30,30 @@ module Gitlab
end end
end end
def should_run_diff_validations? def should_run_validations?
validations_for_diff.present? || path_validations.present? validations_for_path.present? || file_paths_validations.present?
end end
def validate_diff(diff) def validate_path(path)
validations_for_diff.each do |validation| validations_for_path.each do |validation|
if error = validation.call(diff) if error = validation.call(path)
raise ::Gitlab::GitAccess::ForbiddenError, error raise ::Gitlab::GitAccess::ForbiddenError, error
end end
end end
end end
# Method overwritten in EE to inject custom validations # Method overwritten in EE to inject custom validations
def validations_for_diff def validations_for_path
[] []
end end
def path_validations def file_paths_validations
validate_lfs_file_locks? ? [lfs_file_locks_validation] : [] validate_lfs_file_locks? ? [lfs_file_locks_validation] : []
end end
def process_commits
logger.log_timed(LOG_MESSAGES[:diff_content_check]) do
# n+1: https://gitlab.com/gitlab-org/gitlab/issues/3593
::Gitlab::GitalyClient.allow_n_plus_1_calls do
commits.each do |commit|
logger.check_timeout_reached
yield(commit)
end
end
end
end
def validate_file_paths(file_paths) def validate_file_paths(file_paths)
logger.log_timed(LOG_MESSAGES[__method__]) do logger.log_timed(LOG_MESSAGES[__method__]) do
path_validations.each do |validation| file_paths_validations.each do |validation|
if error = validation.call(file_paths) if error = validation.call(file_paths)
raise ::Gitlab::GitAccess::ForbiddenError, error raise ::Gitlab::GitAccess::ForbiddenError, error
end end
......
# frozen_string_literal: true
module Gitlab
module Git
class ChangedPath
attr_reader :status, :path
def initialize(status:, path:)
@status = status
@path = path
end
def new_file?
status == :ADDED
end
end
end
end
...@@ -225,7 +225,7 @@ module Gitlab ...@@ -225,7 +225,7 @@ module Gitlab
response = GitalyClient.call(@repository.storage, :diff_service, :find_changed_paths, request, timeout: GitalyClient.medium_timeout) response = GitalyClient.call(@repository.storage, :diff_service, :find_changed_paths, request, timeout: GitalyClient.medium_timeout)
response.flat_map do |msg| response.flat_map do |msg|
msg.paths.map do |path| msg.paths.map do |path|
OpenStruct.new( Gitlab::Git::ChangedPath.new(
status: path.status, status: path.status,
path: EncodingHelper.encode!(path.path) path: EncodingHelper.encode!(path.path)
) )
......
...@@ -6,96 +6,63 @@ RSpec.describe Gitlab::Checks::DiffCheck do ...@@ -6,96 +6,63 @@ RSpec.describe Gitlab::Checks::DiffCheck do
include_context 'change access checks context' include_context 'change access checks context'
describe '#validate!' do describe '#validate!' do
let(:owner) { create(:user) } context 'when commits is empty' do
it 'does not call find_changed_paths' do
before do expect(project.repository).not_to receive(:find_changed_paths)
allow(project.repository).to receive(:new_commits).and_return(
project.repository.commits_between('be93687618e4b132087f430a4d8fc3a609c9b77c', '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51')
)
end
context 'with LFS not enabled' do
before do
allow(project).to receive(:lfs_enabled?).and_return(false)
end
it 'does not invoke :lfs_file_locks_validation' do
expect(subject).not_to receive(:lfs_file_locks_validation)
subject.validate! subject.validate!
end end
end end
context 'with LFS enabled' do context 'when commits is not empty' do
let!(:lock) { create(:lfs_file_lock, user: owner, project: project, path: 'README') }
before do before do
allow(project).to receive(:lfs_enabled?).and_return(true) allow(project.repository).to receive(:new_commits).and_return(
project.repository.commits_between('be93687618e4b132087f430a4d8fc3a609c9b77c', '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51')
)
end end
context 'when change is sent by a different user' do context 'when deletion is true' do
context 'when diff check with paths rpc feature flag is true' do let(:newrev) { Gitlab::Git::BLANK_SHA }
it 'raises an error if the user is not allowed to update the file' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'README' is locked in Git LFS by #{lock.user.name}")
end
end
context 'when diff check with paths rpc feature flag is false' do it 'does not call find_changed_paths' do
before do expect(project.repository).not_to receive(:find_changed_paths)
stub_feature_flags(diff_check_with_paths_changed_rpc: false)
end
it 'raises an error if the user is not allowed to update the file' do subject.validate!
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'README' is locked in Git LFS by #{lock.user.name}")
end
end end
end end
context 'when change is sent by the author of the lock' do context 'with LFS not enabled' do
let(:user) { owner } before do
allow(project).to receive(:lfs_enabled?).and_return(false)
it "doesn't raise any error" do
expect { subject.validate! }.not_to raise_error
end end
end
end
context 'commit diff validations' do
before do
allow(subject).to receive(:validations_for_diff).and_return([lambda { |diff| return }])
expect_any_instance_of(Commit).to receive(:raw_deltas).and_call_original
stub_feature_flags(diff_check_with_paths_changed_rpc: false)
subject.validate!
end
context 'when request store is inactive' do it 'does not invoke :lfs_file_locks_validation' do
it 'are run for every commit' do expect(subject).not_to receive(:lfs_file_locks_validation)
expect_any_instance_of(Commit).to receive(:raw_deltas).and_call_original
subject.validate! subject.validate!
end end
end end
context 'when request store is active', :request_store do context 'with LFS enabled' do
it 'are cached for every commit' do let(:owner) { create(:user) }
expect_any_instance_of(Commit).not_to receive(:raw_deltas) let!(:lock) { create(:lfs_file_lock, user: owner, project: project, path: 'README') }
subject.validate! before do
allow(project).to receive(:lfs_enabled?).and_return(true)
end end
it 'are run for not cached commits' do context 'when change is sent by a different user' do
allow(project.repository).to receive(:new_commits).and_return( it 'raises an error if the user is not allowed to update the file' do
project.repository.commits_between('be93687618e4b132087f430a4d8fc3a609c9b77c', 'a5391128b0ef5d21df5dd23d98557f4ef12fae20') expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'README' is locked in Git LFS by #{lock.user.name}")
) end
change_access.instance_variable_set(:@commits, project.repository.new_commits) end
expect(project.repository.new_commits.first).not_to receive(:raw_deltas).and_call_original context 'when change is sent by the author of the lock' do
expect(project.repository.new_commits.last).to receive(:raw_deltas).and_call_original let(:user) { owner }
subject.validate! it "doesn't raise any error" do
expect { subject.validate! }.not_to raise_error
end
end end
end end
end end
......
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Gitlab::Git::ChangedPath do
subject(:changed_path) { described_class.new(path: path, status: status) }
let(:path) { 'test_path' }
describe '#new_file?' do
subject(:new_file?) { changed_path.new_file? }
context 'when it is a new file' do
let(:status) { :ADDED }
it 'returns true' do
expect(new_file?).to eq(true)
end
end
context 'when it is not a new file' do
let(:status) { :MODIFIED }
it 'returns false' do
expect(new_file?).to eq(false)
end
end
end
end
...@@ -1191,25 +1191,25 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do ...@@ -1191,25 +1191,25 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
let(:commit_3) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' } let(:commit_3) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' }
let(:commit_1_files) do let(:commit_1_files) do
[ [
OpenStruct.new(status: :ADDED, path: "files/executables/ls"), Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/executables/ls"),
OpenStruct.new(status: :ADDED, path: "files/executables/touch"), Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/executables/touch"),
OpenStruct.new(status: :ADDED, path: "files/links/regex.rb"), Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/links/regex.rb"),
OpenStruct.new(status: :ADDED, path: "files/links/ruby-style-guide.md"), Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/links/ruby-style-guide.md"),
OpenStruct.new(status: :ADDED, path: "files/links/touch"), Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/links/touch"),
OpenStruct.new(status: :MODIFIED, path: ".gitmodules"), Gitlab::Git::ChangedPath.new(status: :MODIFIED, path: ".gitmodules"),
OpenStruct.new(status: :ADDED, path: "deeper/nested/six"), Gitlab::Git::ChangedPath.new(status: :ADDED, path: "deeper/nested/six"),
OpenStruct.new(status: :ADDED, path: "nested/six") Gitlab::Git::ChangedPath.new(status: :ADDED, path: "nested/six")
] ]
end end
let(:commit_2_files) do let(:commit_2_files) do
[OpenStruct.new(status: :ADDED, path: "bin/executable")] [Gitlab::Git::ChangedPath.new(status: :ADDED, path: "bin/executable")]
end end
let(:commit_3_files) do let(:commit_3_files) do
[ [
OpenStruct.new(status: :MODIFIED, path: ".gitmodules"), Gitlab::Git::ChangedPath.new(status: :MODIFIED, path: ".gitmodules"),
OpenStruct.new(status: :ADDED, path: "gitlab-shell") Gitlab::Git::ChangedPath.new(status: :ADDED, path: "gitlab-shell")
] ]
end end
...@@ -1217,7 +1217,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do ...@@ -1217,7 +1217,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
collection = repository.find_changed_paths([commit_1, commit_2, commit_3]) collection = repository.find_changed_paths([commit_1, commit_2, commit_3])
expect(collection).to be_a(Enumerable) expect(collection).to be_a(Enumerable)
expect(collection.to_a).to eq(commit_1_files + commit_2_files + commit_3_files) expect(collection.as_json).to eq((commit_1_files + commit_2_files + commit_3_files).as_json)
end end
it 'returns no paths when SHAs are invalid' do it 'returns no paths when SHAs are invalid' do
...@@ -1231,7 +1231,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do ...@@ -1231,7 +1231,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
collection = repository.find_changed_paths([nil, commit_1]) collection = repository.find_changed_paths([nil, commit_1])
expect(collection).to be_a(Enumerable) expect(collection).to be_a(Enumerable)
expect(collection.to_a).to eq(commit_1_files) expect(collection.as_json).to eq(commit_1_files.as_json)
end end
it 'returns no paths when the commits are nil' do it 'returns no paths when the commits are nil' do
......
...@@ -162,11 +162,9 @@ RSpec.describe Gitlab::GitalyClient::CommitService do ...@@ -162,11 +162,9 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
.with(request, kind_of(Hash)).and_return([changed_paths_response]) .with(request, kind_of(Hash)).and_return([changed_paths_response])
returned_value = described_class.new(repository).find_changed_paths(commits) returned_value = described_class.new(repository).find_changed_paths(commits)
mapped_expected_value = changed_paths_response.paths.map { |path| Gitlab::Git::ChangedPath.new(status: path.status, path: path.path) }
mapped_returned_value = returned_value.map(&:to_h) expect(returned_value.as_json).to eq(mapped_expected_value.as_json)
mapped_expected_value = changed_paths_response.paths.map(&:to_h)
expect(mapped_returned_value).to eq(mapped_expected_value)
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment