Commit b2ef8385 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Allow abilities on forks while MR is open

When an MR is created using `allow_maintainer_to_push`, we enable some
abilities while the MR is open.

This should allow every user with developer abilities on the target
project, to push to the source project.
parent 792ab063
......@@ -150,6 +150,7 @@ class Project < ActiveRecord::Base
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id'
has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
has_many :issues
has_many :labels, class_name: 'ProjectLabel'
has_many :services
......@@ -1799,6 +1800,14 @@ class Project < ActiveRecord::Base
Badge.where("id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
end
def branches_allowing_maintainer_access_to_user(user)
@branches_allowing_maintainer_access_to_user ||= Hash.new do |result, user|
result[user] = fetch_branches_allowing_maintainer_access_to_user(user)
end
@branches_allowing_maintainer_access_to_user[user]
end
private
def storage
......@@ -1921,4 +1930,24 @@ class Project < ActiveRecord::Base
raise ex
end
def fetch_branches_allowing_maintainer_access_to_user(user)
return [] unless user
projects_with_developer_access = user.project_authorizations
.where('access_level >= ? ', Gitlab::Access::DEVELOPER)
.select(:project_id)
merge_requests_allowing_push = source_of_merge_requests.opened
.where(allow_maintainer_to_push: true)
.where(target_project_id: projects_with_developer_access)
.select(:source_branch).uniq
if RequestStore.active?
RequestStore.fetch("project-#{id}:user-#{user.id}:branches_allowing_maintainer_access") do
merge_requests_allowing_push.pluck(:source_branch)
end
else
merge_requests_allowing_push.pluck(:source_branch)
end
end
end
......@@ -61,6 +61,11 @@ class ProjectPolicy < BasePolicy
desc "Project has request access enabled"
condition(:request_access_enabled, scope: :subject) { project.request_access_enabled }
desc "The project has merge requests open that allow external users to push"
condition(:merge_request_allows_push, scope: :subject) do
project.branches_allowing_maintainer_access_to_user(@user).any?
end
features = %w[
merge_requests
issues
......@@ -240,6 +245,7 @@ class ProjectPolicy < BasePolicy
rule { repository_disabled }.policy do
prevent :push_code
prevent :push_single_branch
prevent :download_code
prevent :fork_project
prevent :read_commit_status
......@@ -291,6 +297,16 @@ class ProjectPolicy < BasePolicy
prevent :read_issue
end
# These rules are included to allow maintainers of projects to push to certain
# branches of forks.
rule { can?(:public_access) & merge_request_allows_push }.policy do
enable :push_single_branch
enable :create_build
enable :update_build
enable :create_pipeline
enable :update_pipeline
end
private
def team_member?
......
......@@ -30,9 +30,9 @@ describe Gitlab::Checks::ChangeAccess do
end
end
context 'when the user is not allowed to push code' do
context 'when the user is not allowed to push to the repo' do
it 'raises an error' do
expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false)
expect(user_access).to receive(:can_do_action?).with(:push_to_repo).and_return(false)
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.')
end
......@@ -42,7 +42,7 @@ describe Gitlab::Checks::ChangeAccess do
let(:ref) { 'refs/tags/v1.0.0' }
it 'raises an error if the user is not allowed to update tags' do
allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true)
allow(user_access).to receive(:can_do_action?).with(:push_to_repo).and_return(true)
expect(user_access).to receive(:can_do_action?).with(:admin_project).and_return(false)
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to change existing tags on this project.')
......
......@@ -278,6 +278,7 @@ project:
- custom_attributes
- lfs_file_locks
- project_badges
- source_of_merge_requests
award_emoji:
- awardable
- user
......
require 'spec_helper'
describe Project do
include ProjectForksHelper
describe 'associations' do
it { is_expected.to belong_to(:group) }
it { is_expected.to belong_to(:namespace) }
......@@ -3378,4 +3380,54 @@ describe Project do
end
end
end
describe '#branches_allowing_maintainer_access_to_user' do
let(:maintainer) { create(:user) }
let(:target_project) { create(:project) }
let(:project) { fork_project(target_project) }
let!(:merge_request) do
create(
:merge_request,
target_project: target_project,
source_project: project,
source_branch: 'awesome-feature-1',
allow_maintainer_to_push: true
)
end
before do
target_project.add_developer(maintainer)
end
it 'includes branch names for merge requests allowing maintainer access to a user' do
expect(project.branches_allowing_maintainer_access_to_user(maintainer))
.to include('awesome-feature-1')
end
it 'does not include branches for closed MRs' do
create(:merge_request, :closed,
target_project: target_project,
source_project: project,
source_branch: 'rejected-feature-1',
allow_maintainer_to_push: true)
expect(project.branches_allowing_maintainer_access_to_user(maintainer))
.not_to include('rejected-feature-1')
end
it 'only queries once per user' do
expect { 3.times { project.branches_allowing_maintainer_access_to_user(maintainer) } }
.not_to exceed_query_limit(1)
end
context 'when the requeststore is active', :request_store do
it 'only queries once per user accross project instances' do
# limiting to 3 queries:
# 2 times loading the project
# once loading the accessible branches
expect { 2.times { described_class.find(project.id).branches_allowing_maintainer_access_to_user(maintainer) } }
.not_to exceed_query_limit(3)
end
end
end
end
......@@ -308,4 +308,41 @@ describe ProjectPolicy do
it_behaves_like 'project policies as master'
it_behaves_like 'project policies as owner'
it_behaves_like 'project policies as admin'
context 'when a public project has merge requests allowing access' do
include ProjectForksHelper
let(:user) { create(:user) }
let(:target_project) { create(:project, :public) }
let(:project) { fork_project(target_project) }
let!(:merge_request) do
create(
:merge_request,
target_project: target_project,
source_project: project,
allow_maintainer_to_push: true
)
end
let(:maintainer_abilities) do
%w(push_single_branch create_build update_build create_pipeline update_pipeline)
end
subject { described_class.new(user, project) }
it 'does not allow pushing code' do
expect_disallowed(*maintainer_abilities)
end
it 'allows pushing if the user is a member with push access to the target project' do
target_project.add_developer(user)
expect_allowed(*maintainer_abilities)
end
it 'dissallows abilities to a maintainer if the merge request was closed' do
target_project.add_developer(user)
merge_request.close!
expect_disallowed(*maintainer_abilities)
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