Commit f79f3a1d authored by Timothy Andrew's avatar Timothy Andrew

Fix branch protection API.

1. Previously, we were not removing existing access levels before
   creating new ones. This is not a problem for EE, but _is_ for CE,
   since we restrict the number of access levels in CE to 1.

2. The correct approach is:

    CE -> delete all access levels before updating a protected branch
    EE -> delete developer access levels if "developers_can_{merge,push}" is switched off

3. The dispatch is performed by checking if a "length: 1" validation is
   present on the access levels or not.

4. Another source of problems was that we didn't put multiple queries in
   a transaction. If the `destroy_all` passes, but the `update` fails,
   we should have a rollback.

5. Modifying the API to provide users direct access to CRUD access
   levels will make things a lot simpler.

6. Create `create/update` services separately for this API, which
   perform the necessary data translation, before calling the regular
   `create/update` services. The translation code was getting too large
   for the API endpoint itself, so this move makes sense.
parent a98ad03b
module ProtectedBranchAccess
extend ActiveSupport::Concern
included do
scope :master, -> () { where(access_level: Gitlab::Access::MASTER) }
scope :developer, -> () { where(access_level: Gitlab::Access::DEVELOPER) }
end
def humanize
self.class.human_access_levels[self.access_level]
end
......
# The protected branches API still uses the `developers_can_push` and `developers_can_merge`
# flags for backward compatibility, and so performs translation between that format and the
# internal data model (separate access levels). The translation code is non-trivial, and so
# lives in this service.
module ProtectedBranches
class ApiCreateService < BaseService
def initialize(project, user, params, developers_can_push:, developers_can_merge:)
super(project, user, params)
@developers_can_merge = developers_can_merge
@developers_can_push = developers_can_push
end
def execute
if @developers_can_push
@params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }])
else
@params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }])
end
if @developers_can_merge
@params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }])
else
@params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }])
end
service = ProtectedBranches::CreateService.new(@project, @current_user, @params)
service.execute
end
end
end
# The protected branches API still uses the `developers_can_push` and `developers_can_merge`
# flags for backward compatibility, and so performs translation between that format and the
# internal data model (separate access levels). The translation code is non-trivial, and so
# lives in this service.
module ProtectedBranches
class ApiUpdateService < BaseService
def initialize(project, user, params, developers_can_push:, developers_can_merge:)
super(project, user, params)
@developers_can_merge = developers_can_merge
@developers_can_push = developers_can_push
end
def execute(protected_branch)
@protected_branch = protected_branch
protected_branch.transaction do
# If a protected branch can have only a single access level (CE), delete it to
# make room for the new access level. If a protected branch can have more than
# one access level (EE), only remove the relevant access levels. If we don't do this,
# we'll have a failed validation.
if protected_branch_restricted_to_single_access_level?
delete_redundant_ce_access_levels
else
delete_redundant_ee_access_levels
end
if @developers_can_push
params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }])
elsif @developers_can_push == false
params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }])
end
if @developers_can_merge
params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }])
elsif @developers_can_merge == false
params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }])
end
service = ProtectedBranches::UpdateService.new(@project, @current_user, @params)
service.execute(protected_branch)
end
end
private
def delete_redundant_ce_access_levels
if @developers_can_merge || @developers_can_merge == false
@protected_branch.merge_access_levels.destroy_all
end
if @developers_can_push || @developers_can_push == false
@protected_branch.push_access_levels.destroy_all
end
end
def delete_redundant_ee_access_levels
if @developers_can_merge
@protected_branch.merge_access_levels.developer.destroy_all
elsif @developers_can_merge == false
@protected_branch.merge_access_levels.developer.destroy_all
@protected_branch.merge_access_levels.master.destroy_all
end
if @developers_can_push
@protected_branch.push_access_levels.developer.destroy_all
elsif @developers_can_push == false
@protected_branch.push_access_levels.developer.destroy_all
@protected_branch.push_access_levels.master.destroy_all
end
end
def protected_branch_restricted_to_single_access_level?
length_validator = ProtectedBranch.validators_on(:push_access_levels).find do |validator|
validator.is_a? ActiveModel::Validations::LengthValidator
end
length_validator.options[:is] == 1 if length_validator
end
end
end
......@@ -57,40 +57,25 @@ module API
developers_can_merge = to_boolean(params[:developers_can_merge])
developers_can_push = to_boolean(params[:developers_can_push])
protected_branch_params = {
name: @branch.name
params = {
name: @branch.name,
}
# If `developers_can_merge` is switched off, _all_ `DEVELOPER`
# merge_access_levels need to be deleted.
if developers_can_merge == false
protected_branch.merge_access_levels.where(access_level: Gitlab::Access::DEVELOPER).destroy_all
end
service_args = [user_project, current_user, params,
developers_can_push: developers_can_push,
developers_can_merge: developers_can_merge]
# If `developers_can_push` is switched off, _all_ `DEVELOPER`
# push_access_levels need to be deleted.
if developers_can_push == false
protected_branch.push_access_levels.where(access_level: Gitlab::Access::DEVELOPER).destroy_all
end
protected_branch = if protected_branch
ProtectedBranches::ApiUpdateService.new(*service_args).execute(protected_branch)
else
ProtectedBranches::ApiCreateService.new(*service_args).execute
end
protected_branch_params.merge!(
merge_access_levels_attributes: [{
access_level: developers_can_merge ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
}],
push_access_levels_attributes: [{
access_level: developers_can_push ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
}]
)
if protected_branch
service = ProtectedBranches::UpdateService.new(user_project, current_user, protected_branch_params)
service.execute(protected_branch)
if protected_branch.valid?
present @branch, with: Entities::RepoBranch, project: user_project
else
service = ProtectedBranches::CreateService.new(user_project, current_user, protected_branch_params)
service.execute
render_api_error!(protected_branch.errors.full_messages, 422)
end
present @branch, with: Entities::RepoBranch, project: user_project
end
# Unprotect a single branch
......@@ -123,7 +108,7 @@ module API
post ":id/repository/branches" do
authorize_push_project
result = CreateBranchService.new(user_project, current_user).
execute(params[:branch_name], params[:ref])
execute(params[:branch_name], params[:ref])
if result[:status] == :success
present result[:branch],
......@@ -142,10 +127,10 @@ module API
# Example Request:
# DELETE /projects/:id/repository/branches/:branch
delete ":id/repository/branches/:branch",
requirements: { branch: /.+/ } do
requirements: { branch: /.+/ } do
authorize_push_project
result = DeleteBranchService.new(user_project, current_user).
execute(params[:branch])
execute(params[:branch])
if result[:status] == :success
{
......
This diff is collapsed.
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