Commit 242f8377 authored by Rémy Coutable's avatar Rémy Coutable
Browse files

Merge branch '18193-no-one-can-push' into 'master'

Allow creating protected branches that can't be pushed to

## What does this MR do?

- Add "No one can push" as a setting to protected branches.
- This applies to Masters (as well as all other users)

## What are the relevant issue numbers?

Closes #18193 

## Does this need an EE merge request?

Yes. gitlab-org/gitlab-ee!569

## Screenshots

![Screen_Shot_2016-07-29_at_3.14.59_PM](/uploads/2e8774c311763bc6e570501a2e6cabf7/Screen_Shot_2016-07-29_at_3.14.59_PM.png)

## TODO

- [ ]  #18193 !5081 No one can push to protected branches
    - [x]  Implementation
        - [x]  Model changes
            - [x]  Remove "developers_can_merge" and "developers_can_push"
            - [x]  Replace with `ProtectedBranchPushAccess` and `ProtectedBranchMergeAccess`
                - [x]  Reversible migration
                - [x]  Raise error on failure
                - [x]  MySQL
        - [x]  Backend changes
            - [x]  Creating a protected branch creates access rows
            - [x]  Add `no_one` as an access level
            - [x]  Enforce "no one can push" 
            - [x]  Allow setting levels while creating protected branches?
        - [x]  Frontend
            - [x]  Replace checkboxes with `select`s
    - [x]  Add tests
    - [x]  `GitPushService` -> new projects' default branch protection
    - [x]  Fix existing tests
    - [x]  Refactor
    - [x]  Test workflows by hand
        - [x]  from the Web UI
            - [x]  When "Allowed to Push" is "No one"
                - [x]  Developers can't push
                - [x]  Masters can't push
            - [x]  When "Allowed to Push" is "Developers + Masters"
                - [x]  Developers can push
                - [x]  Masters can push
            - [x]  When "Allowed to Push" is "Masters"
                - [x]  Developers can't push
                - [x]  Masters can push
            - [x]  When "Allowed to Merge" is "Masters" and "Allowed to Push" is "Masters"
                - [x]  Developers can't push
                - [x]  Developers can't merge
                - [x]  Masters can merge
                - [x]  Masters can push
            - [x]  When "Allowed to Merge" is "Developers + Masters" and "Allowed to Push" is "Masters"
                - [x]  Developers can't push
                - [x]  Developers can merge
                - [x]  Masters can merge
                - [x]  Masters can push
            - [x]  When "Allowed to Merge" is "Developers + Masters" and "Allowed to Push" is "No one"
                - [x]  Developers can't push
                - [x]  Developers can merge
                - [x]  Masters can merge
                - [x]  Masters can't push
            - [x]  When "Allowed to Merge" is "Masters" and "Allowed to Push" is "No one"
                - [x]  Developers can't push
                - [x]  Developers can't merge
                - [x]  Masters can merge
                - [x]  Masters can't push
        - [x]  from CLI
            - [x]  When "Allowed to Push" is "No one"
                - [x]  Developers can't push
                - [x]  Masters can't push
            - [x]  When "Allowed to Push" is "Developers + Masters"
                - [x]  Developers can push
                - [x]  Masters can push
            - [x]  When "Allowed to Push" is "Masters"
                - [x]  Developers can't push
                - [x]  Masters can push
            - [x]  When "Allowed to Merge" is "Masters" and "Allowed to Push" is "Masters"
                - [x]  Developers can't push
                - [x]  Developers can't merge
                - [x]  Masters can merge
                - [x]  Masters can push
            - [x]  When "Allowed to Merge" is "Developers + Masters" and "Allowed to Push" is "Masters"
                - [x]  Developers can't push
                - [x]  Developers can't merge
                - [x]  Masters can merge
                - [x]  Masters can push
            - [x]  When "Allowed to Merge" is "Developers + Masters" and "Allowed to Push" is "No one"
                - [x]  Developers can't push
                - [x]  Developers can't merge
                - [x]  Masters can't merge
                - [x]  Masters can't push
            - [x]  When "Allowed to Merge" is "Masters" and "Allowed to Push" is "No one"
                - [x]  Developers can't push
                - [x]  Developers can't merge
                - [x]  Masters can't merge
                - [x]  Masters can't push
    - [x]  Add tests for owners and admins
    - [x]  CHANGELOG
    - [x]  Screenshots
    - [x]  Documentation
    - [x]  Wait for ~~!4665~~ to be merged in
    - [x]  Wait for ~~gitlab-org/gitlab-ce#19872~~ and ~~gitlab-org/gitlab-ee!564~~ to be closed
    - [x]  Rebase against master instead of !4892
    - [x]  Make sure [build](https://gitlab.com/gitlab-org/gitlab-ce/commit/a4ca206fd1cc0332d1e385ddbc0f2e4065c3ae73/builds) is green
    - [x]  Create EE MR
        - [x]  Cherry pick commits
        - [x]  Make sure [build](https://gitlab.com/gitlab-org/gitlab-ee/commit/4e17190d7dc546c1f977edcafd1cbcea4bdb4043/builds) is green
    - [x]  Address @axil's comments
    - [x]  Assign to endboss
    - [x]  Wait for @dbalexandre's review
    - [x]  Address @dbalexandre's comments
    - [x]  Address @axil's comments
        - [x]  Align dropdowns
        - [x]  No flash when protected branch is updated
    - [x]  Resolve conflicts
    - [x]  Implement protect/unprotect API
    - [x]  Address @dbalexandre's comments
    - [x]  Update EE MR
    - [x]  Address @rymai's comments
        - [x]  Create/Update service should return a `ProtectedBranch`
        - [x]  Successfuly protected branch creation shouldn't `load_protected_branches`
        - [x]  Rename `allowed_to_merge` as #minimum_access_level_for_merge
        - [x]  Rename `allowed_to_push` as #minimum_access_level_for_push
        - [x]  Use `inclusion` and `Gitlab::Access` instead of an `enum`
        - [x]  Modify `check_access` to work with `Gitlab::Access`
        - [x]  Pass `@protected_branch` to `#execute` in `UpdateService`
        - [x]  simplify with a nested field `protected_branch[push_access_level][access_level]`
        - [x]  `developers_can_{merge,push}` should be handled in the API
        - [x]  Use `can?(current_user, ...)` instead of `current_user.can?(...)`
        - [x]  Instantiate `ProtectedBranchesAccessSelect` in `dispatcher.js`
        - [x]  constants regarding downtime migrations
        - [x]  Explicit `#down` for columns with default
    - [x]  Update EE MR
    - [ ]  Wait for CE merge
    - [ ]  Wait for EE merge
    - [ ]  Create issue for UI changes proposed by @zyv


See merge request !5081
parents 9b0e131b cebcc417
......@@ -8,6 +8,7 @@ v 8.11.0 (unreleased)
- Fix of 'Commits being passed to custom hooks are already reachable when using the UI'
- Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable
- Optimize maximum user access level lookup in loading of notes
- Add "No one can push" as an option for protected branches. !5081
- Limit git rev-list output count to one in forced push check
- Clean up unused routes (Josef Strzibny)
- Add green outline to New Branch button. !5447 (winniehell)
......
......@@ -171,6 +171,11 @@
break;
case 'search:show':
new Search();
break;
case 'projects:protected_branches:index':
new ProtectedBranchesAccessSelect($(".new_protected_branch"), false, true);
new ProtectedBranchesAccessSelect($(".protected-branches-list"), true, false);
break;
}
switch (path.first()) {
case 'admin':
......
(function() {
$(function() {
return $(".protected-branches-list :checkbox").change(function(e) {
var can_push, id, name, obj, url;
name = $(this).attr("name");
if (name === "developers_can_push" || name === "developers_can_merge") {
id = $(this).val();
can_push = $(this).is(":checked");
url = $(this).data("url");
return $.ajax({
type: "PATCH",
url: url,
dataType: "json",
data: {
id: id,
protected_branch: (
obj = {},
obj["" + name] = can_push,
obj
)
},
success: function() {
var row;
row = $(e.target);
return row.closest('tr').effect('highlight');
},
error: function() {
return new Flash("Failed to update branch!", "alert");
}
});
}
});
});
}).call(this);
class ProtectedBranchesAccessSelect {
constructor(container, saveOnSelect, selectDefault) {
this.container = container;
this.saveOnSelect = saveOnSelect;
this.container.find(".allowed-to-merge").each((i, element) => {
var fieldName = $(element).data('field-name');
var dropdown = $(element).glDropdown({
data: gon.merge_access_levels,
selectable: true,
fieldName: fieldName,
clicked: _.chain(this.onSelect).partial(element).bind(this).value()
});
if (selectDefault) {
dropdown.data('glDropdown').selectRowAtIndex(document.createEvent("Event"), 0);
}
});
this.container.find(".allowed-to-push").each((i, element) => {
var fieldName = $(element).data('field-name');
var dropdown = $(element).glDropdown({
data: gon.push_access_levels,
selectable: true,
fieldName: fieldName,
clicked: _.chain(this.onSelect).partial(element).bind(this).value()
});
if (selectDefault) {
dropdown.data('glDropdown').selectRowAtIndex(document.createEvent("Event"), 0);
}
});
}
onSelect(dropdown, selected, element, e) {
$(dropdown).find('.dropdown-toggle-text').text(selected.text);
if (this.saveOnSelect) {
return $.ajax({
type: "POST",
url: $(dropdown).data('url'),
dataType: "json",
data: {
_method: 'PATCH',
id: $(dropdown).data('id'),
protected_branch: {
["" + ($(dropdown).data('type')) + "_attributes"]: {
"access_level": selected.id
}
}
},
success: function() {
var row;
row = $(e.target);
return row.closest('tr').effect('highlight');
},
error: function() {
return new Flash("Failed to update branch!", "alert");
}
});
}
}
}
......@@ -661,14 +661,28 @@ pre.light-well {
}
}
.new_protected_branch {
.dropdown {
display: inline;
margin-left: 15px;
}
label {
min-width: 120px;
}
}
.protected-branches-list {
a {
color: $gl-gray;
font-weight: 600;
&:hover {
color: $gl-link-color;
}
&.is-active {
font-weight: 600;
}
}
}
......
......@@ -3,19 +3,24 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :authorize_admin_project!
before_action :load_protected_branch, only: [:show, :update, :destroy]
before_action :load_protected_branches, only: [:index]
layout "project_settings"
def index
@protected_branches = @project.protected_branches.order(:name).page(params[:page])
@protected_branch = @project.protected_branches.new
gon.push({ open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } })
load_protected_branches_gon_variables
end
def create
@project.protected_branches.create(protected_branch_params)
redirect_to namespace_project_protected_branches_path(@project.namespace,
@project)
@protected_branch = ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute
if @protected_branch.persisted?
redirect_to namespace_project_protected_branches_path(@project.namespace, @project)
else
load_protected_branches
load_protected_branches_gon_variables
render :index
end
end
def show
......@@ -23,7 +28,9 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
end
def update
if @protected_branch && @protected_branch.update_attributes(protected_branch_params)
@protected_branch = ProtectedBranches::UpdateService.new(@project, current_user, protected_branch_params).execute(@protected_branch)
if @protected_branch.valid?
respond_to do |format|
format.json { render json: @protected_branch, status: :ok }
end
......@@ -50,6 +57,18 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
end
def protected_branch_params
params.require(:protected_branch).permit(:name, :developers_can_push, :developers_can_merge)
params.require(:protected_branch).permit(:name,
merge_access_level_attributes: [:access_level],
push_access_level_attributes: [:access_level])
end
def load_protected_branches
@protected_branches = @project.protected_branches.order(:name).page(params[:page])
end
def load_protected_branches_gon_variables
gon.push({ open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } },
push_access_levels: ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text } },
merge_access_levels: ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text } } })
end
end
......@@ -874,14 +874,6 @@ class Project < ActiveRecord::Base
ProtectedBranch.matching(branch_name, protected_branches: @protected_branches).present?
end
def developers_can_push_to_protected_branch?(branch_name)
protected_branches.matching(branch_name).any?(&:developers_can_push)
end
def developers_can_merge_to_protected_branch?(branch_name)
protected_branches.matching(branch_name).any?(&:developers_can_merge)
end
def forked?
!(forked_project_link.nil? || forked_project_link.forked_from_project.nil?)
end
......
......@@ -5,6 +5,12 @@ class ProtectedBranch < ActiveRecord::Base
validates :name, presence: true
validates :project, presence: true
has_one :merge_access_level, dependent: :destroy
has_one :push_access_level, dependent: :destroy
accepts_nested_attributes_for :push_access_level
accepts_nested_attributes_for :merge_access_level
def commit
project.commit(self.name)
end
......
class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base
belongs_to :protected_branch
delegate :project, to: :protected_branch
validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER,
Gitlab::Access::DEVELOPER] }
def self.human_access_levels
{
Gitlab::Access::MASTER => "Masters",
Gitlab::Access::DEVELOPER => "Developers + Masters"
}.with_indifferent_access
end
def check_access(user)
return true if user.is_admin?
project.team.max_member_access(user.id) >= access_level
end
def humanize
self.class.human_access_levels[self.access_level]
end
end
class ProtectedBranch::PushAccessLevel < ActiveRecord::Base
belongs_to :protected_branch
delegate :project, to: :protected_branch
validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER,
Gitlab::Access::DEVELOPER,
Gitlab::Access::NO_ACCESS] }
def self.human_access_levels
{
Gitlab::Access::MASTER => "Masters",
Gitlab::Access::DEVELOPER => "Developers + Masters",
Gitlab::Access::NO_ACCESS => "No one"
}.with_indifferent_access
end
def check_access(user)
return false if access_level == Gitlab::Access::NO_ACCESS
return true if user.is_admin?
project.team.max_member_access(user.id) >= access_level
end
def humanize
self.class.human_access_levels[self.access_level]
end
end
......@@ -88,9 +88,18 @@ class GitPushService < BaseService
# Set protection on the default branch if configured
if current_application_settings.default_branch_protection != PROTECTION_NONE
developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false
developers_can_merge = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_MERGE ? true : false
@project.protected_branches.create({ name: @project.default_branch, developers_can_push: developers_can_push, developers_can_merge: developers_can_merge })
params = {
name: @project.default_branch,
push_access_level_attributes: {
access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
},
merge_access_level_attributes: {
access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
}
}
ProtectedBranches::CreateService.new(@project, current_user, params).execute
end
end
......
module ProtectedBranches
class CreateService < BaseService
attr_reader :protected_branch
def execute
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
protected_branch = project.protected_branches.new(params)
ProtectedBranch.transaction do
protected_branch.save!
if protected_branch.push_access_level.blank?
protected_branch.create_push_access_level!(access_level: Gitlab::Access::MASTER)
end
if protected_branch.merge_access_level.blank?
protected_branch.create_merge_access_level!(access_level: Gitlab::Access::MASTER)
end
end
protected_branch
rescue ActiveRecord::RecordInvalid
protected_branch
end
end
end
module ProtectedBranches
class UpdateService < BaseService
attr_reader :protected_branch
def execute(protected_branch)
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
@protected_branch = protected_branch
@protected_branch.update(params)
@protected_branch
end
end
end
......@@ -5,24 +5,22 @@
No branches are protected, protect a branch with the form above.
- else
- can_admin_project = can?(current_user, :admin_project, @project)
.table-responsive
%table.table.protected-branches-list
%colgroup
%col{ width: "20%" }
%col{ width: "30%" }
%col{ width: "25%" }
%col{ width: "25%" }
%table.table.protected-branches-list
%colgroup
%col{ width: "20%" }
%col{ width: "30%" }
%col{ width: "25%" }
%col{ width: "25%" }
%thead
%tr
%th Branch
%th Last commit
%th Allowed to merge
%th Allowed to push
- if can_admin_project
%col
%thead
%tr
%th Protected Branch
%th Commit
%th Developers Can Push
%th Developers Can Merge
- if can_admin_project
%th
%tbody
= render partial: @protected_branches, locals: { can_admin_project: can_admin_project }
%th
%tbody
= render partial: @protected_branches, locals: { can_admin_project: can_admin_project }
= paginate @protected_branches, theme: 'gitlab'
......@@ -15,9 +15,15 @@
- else
(branch was removed from repository)
%td
= check_box_tag("developers_can_push", protected_branch.id, protected_branch.developers_can_push, data: { url: url })
= hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_level.access_level
= dropdown_tag(protected_branch.merge_access_level.humanize,
options: { title: "Allowed to merge", toggle_class: 'allowed-to-merge', dropdown_class: 'dropdown-menu-selectable merge',
data: { field_name: "allowed_to_merge_#{protected_branch.id}", url: url, id: protected_branch.id, type: "merge_access_level" }})
%td
= check_box_tag("developers_can_merge", protected_branch.id, protected_branch.developers_can_merge, data: { url: url })
= hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_level.access_level
= dropdown_tag(protected_branch.push_access_level.humanize,
options: { title: "Allowed to push", toggle_class: 'allowed-to-push', dropdown_class: 'dropdown-menu-selectable push',
data: { field_name: "allowed_to_push_#{protected_branch.id}", url: url, id: protected_branch.id, type: "push_access_level" }})
- if can_admin_project
%td
= link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-warning btn-sm pull-right"
......@@ -32,18 +32,22 @@
are supported.
.form-group
= f.check_box :developers_can_push, class: "pull-left"
.prepend-left-20
= f.label :developers_can_push, "Developers can push", class: "label-light append-bottom-0"
%p.light.append-bottom-0
Allow developers to push to this branch
= hidden_field_tag 'protected_branch[merge_access_level_attributes][access_level]'
= label_tag "Allowed to merge: ", nil, class: "label-light append-bottom-0"
= dropdown_tag("<Make a selection>",
options: { title: "Allowed to merge", toggle_class: 'allowed-to-merge',
dropdown_class: 'dropdown-menu-selectable',
data: { field_name: "protected_branch[merge_access_level_attributes][access_level]" }})
.form-group
= f.check_box :developers_can_merge, class: "pull-left"
.prepend-left-20
= f.label :developers_can_merge, "Developers can merge", class: "label-light append-bottom-0"
%p.light.append-bottom-0
Allow developers to accept merge requests to this branch
= hidden_field_tag 'protected_branch[push_access_level_attributes][access_level]'
= label_tag "Allowed to push: ", nil, class: "label-light append-bottom-0"
= dropdown_tag("<Make a selection>",
options: { title: "Allowed to push", toggle_class: 'allowed-to-push',
dropdown_class: 'dropdown-menu-selectable',
data: { field_name: "protected_branch[push_access_level_attributes][access_level]" }})
= f.submit "Protect", class: "btn-create btn protect-branch-btn", disabled: true
%hr
......
Gitlab::Seeder.quiet do
admin_user = User.find(1)
Project.all.each do |project|
params = {
name: 'master'
}
ProtectedBranches::CreateService.new(project, admin_user, params).execute
print '.'
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddProtectedBranchesPushAccess < ActiveRecord::Migration
DOWNTIME = false
def change
create_table :protected_branch_push_access_levels do |t|
t.references :protected_branch, index: { name: "index_protected_branch_push_access" }, foreign_key: true, null: false
# Gitlab::Access::MASTER == 40
t.integer :access_level, default: 40, null: false
t.timestamps null: false
end
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddProtectedBranchesMergeAccess < ActiveRecord::Migration
DOWNTIME = false
def change
create_table :protected_branch_merge_access_levels do |t|
t.references :protected_branch, index: { name: "index_protected_branch_merge_access" }, foreign_key: true, null: false
# Gitlab::Access::MASTER == 40
t.integer :access_level, default: 40, null: false
t.timestamps null: false
end
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class MoveFromDevelopersCanMergeToProtectedBranchesMergeAccess < ActiveRecord::Migration
DOWNTIME = true
DOWNTIME_REASON = <<-HEREDOC
We're creating a `merge_access_level` for each `protected_branch`. If a user creates a `protected_branch` while this
is running, we might be left with a `protected_branch` _without_ an associated `merge_access_level`. The `protected_branches`
table must not change while this is running, so downtime is required.
https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5081#note_13247410
HEREDOC
def up
execute <<-HEREDOC
INSERT into protected_branch_merge_access_levels (protected_branch_id, access_level, created_at, updated_at)
SELECT id, (CASE WHEN developers_can_merge THEN 1 ELSE 0 END), now(), now()
FROM protected_branches
HEREDOC
end
def down
execute <<-HEREDOC
UPDATE protected_branches SET developers_can_merge = TRUE
WHERE id IN (SELECT protected_branch_id FROM protected_branch_merge_access_levels
WHERE access_level = 1);
HEREDOC
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