Merge branch '18471-restrict-tag-pushes-protected-tags' into 'master'
Protected Tags Closes #18471 See merge request !10356
This commit is contained in:
commit
46aadc5c16
83 changed files with 1466 additions and 297 deletions
|
@ -44,6 +44,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
|
|||
import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
|
||||
import BlobForkSuggestion from './blob/blob_fork_suggestion';
|
||||
import UserCallout from './user_callout';
|
||||
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
|
||||
|
||||
const ShortcutsBlob = require('./shortcuts_blob');
|
||||
|
||||
|
@ -329,8 +330,12 @@ const ShortcutsBlob = require('./shortcuts_blob');
|
|||
new Search();
|
||||
break;
|
||||
case 'projects:repository:show':
|
||||
// Initialize Protected Branch Settings
|
||||
new gl.ProtectedBranchCreate();
|
||||
new gl.ProtectedBranchEditList();
|
||||
// Initialize Protected Tag Settings
|
||||
new ProtectedTagCreate();
|
||||
new ProtectedTagEditList();
|
||||
break;
|
||||
case 'projects:ci_cd:show':
|
||||
new gl.ProjectVariables();
|
||||
|
|
2
app/assets/javascripts/protected_tags/index.js
Normal file
2
app/assets/javascripts/protected_tags/index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default as ProtectedTagCreate } from './protected_tag_create';
|
||||
export { default as ProtectedTagEditList } from './protected_tag_edit_list';
|
|
@ -0,0 +1,26 @@
|
|||
export default class ProtectedTagAccessDropdown {
|
||||
constructor(options) {
|
||||
this.options = options;
|
||||
this.initDropdown();
|
||||
}
|
||||
|
||||
initDropdown() {
|
||||
const { onSelect } = this.options;
|
||||
this.options.$dropdown.glDropdown({
|
||||
data: this.options.data,
|
||||
selectable: true,
|
||||
inputId: this.options.$dropdown.data('input-id'),
|
||||
fieldName: this.options.$dropdown.data('field-name'),
|
||||
toggleLabel(item, $el) {
|
||||
if ($el.is('.is-active')) {
|
||||
return item.text;
|
||||
}
|
||||
return 'Select';
|
||||
},
|
||||
clicked(item, $el, e) {
|
||||
e.preventDefault();
|
||||
onSelect();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
|
||||
import ProtectedTagDropdown from './protected_tag_dropdown';
|
||||
|
||||
export default class ProtectedTagCreate {
|
||||
constructor() {
|
||||
this.$form = $('.js-new-protected-tag');
|
||||
this.buildDropdowns();
|
||||
}
|
||||
|
||||
buildDropdowns() {
|
||||
const $allowedToCreateDropdown = this.$form.find('.js-allowed-to-create');
|
||||
|
||||
// Cache callback
|
||||
this.onSelectCallback = this.onSelect.bind(this);
|
||||
|
||||
// Allowed to Create dropdown
|
||||
this.protectedTagAccessDropdown = new ProtectedTagAccessDropdown({
|
||||
$dropdown: $allowedToCreateDropdown,
|
||||
data: gon.create_access_levels,
|
||||
onSelect: this.onSelectCallback,
|
||||
});
|
||||
|
||||
// Select default
|
||||
$allowedToCreateDropdown.data('glDropdown').selectRowAtIndex(0);
|
||||
|
||||
// Protected tag dropdown
|
||||
this.protectedTagDropdown = new ProtectedTagDropdown({
|
||||
$dropdown: this.$form.find('.js-protected-tag-select'),
|
||||
onSelect: this.onSelectCallback,
|
||||
});
|
||||
}
|
||||
|
||||
// This will run after clicked callback
|
||||
onSelect() {
|
||||
// Enable submit button
|
||||
const $tagInput = this.$form.find('input[name="protected_tag[name]"]');
|
||||
const $allowedToCreateInput = this.$form.find('#create_access_levels_attributes');
|
||||
|
||||
this.$form.find('input[type="submit"]').attr('disabled', !($tagInput.val() && $allowedToCreateInput.length));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
export default class ProtectedTagDropdown {
|
||||
/**
|
||||
* @param {Object} options containing
|
||||
* `$dropdown` target element
|
||||
* `onSelect` event callback
|
||||
* $dropdown must be an element created using `dropdown_tag()` rails helper
|
||||
*/
|
||||
constructor(options) {
|
||||
this.onSelect = options.onSelect;
|
||||
this.$dropdown = options.$dropdown;
|
||||
this.$dropdownContainer = this.$dropdown.parent();
|
||||
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
|
||||
this.$protectedTag = this.$dropdownContainer.find('.create-new-protected-tag');
|
||||
|
||||
this.buildDropdown();
|
||||
this.bindEvents();
|
||||
|
||||
// Hide footer
|
||||
this.toggleFooter(true);
|
||||
}
|
||||
|
||||
buildDropdown() {
|
||||
this.$dropdown.glDropdown({
|
||||
data: this.getProtectedTags.bind(this),
|
||||
filterable: true,
|
||||
remote: false,
|
||||
search: {
|
||||
fields: ['title'],
|
||||
},
|
||||
selectable: true,
|
||||
toggleLabel(selected) {
|
||||
return (selected && 'id' in selected) ? selected.title : 'Protected Tag';
|
||||
},
|
||||
fieldName: 'protected_tag[name]',
|
||||
text(protectedTag) {
|
||||
return _.escape(protectedTag.title);
|
||||
},
|
||||
id(protectedTag) {
|
||||
return _.escape(protectedTag.id);
|
||||
},
|
||||
onFilter: this.toggleCreateNewButton.bind(this),
|
||||
clicked: (item, $el, e) => {
|
||||
e.preventDefault();
|
||||
this.onSelect();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.$protectedTag.on('click', this.onClickCreateWildcard.bind(this));
|
||||
}
|
||||
|
||||
onClickCreateWildcard(e) {
|
||||
this.$dropdown.data('glDropdown').remote.execute();
|
||||
this.$dropdown.data('glDropdown').selectRowAtIndex();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
getProtectedTags(term, callback) {
|
||||
if (this.selectedTag) {
|
||||
callback(gon.open_tags.concat(this.selectedTag));
|
||||
} else {
|
||||
callback(gon.open_tags);
|
||||
}
|
||||
}
|
||||
|
||||
toggleCreateNewButton(tagName) {
|
||||
if (tagName) {
|
||||
this.selectedTag = {
|
||||
title: tagName,
|
||||
id: tagName,
|
||||
text: tagName,
|
||||
};
|
||||
|
||||
this.$dropdownContainer
|
||||
.find('.create-new-protected-tag code')
|
||||
.text(tagName);
|
||||
}
|
||||
|
||||
this.toggleFooter(!tagName);
|
||||
}
|
||||
|
||||
toggleFooter(toggleState) {
|
||||
this.$dropdownFooter.toggleClass('hidden', toggleState);
|
||||
}
|
||||
}
|
52
app/assets/javascripts/protected_tags/protected_tag_edit.js
Normal file
52
app/assets/javascripts/protected_tags/protected_tag_edit.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
/* eslint-disable no-new */
|
||||
/* global Flash */
|
||||
|
||||
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
|
||||
|
||||
export default class ProtectedTagEdit {
|
||||
constructor(options) {
|
||||
this.$wrap = options.$wrap;
|
||||
this.$allowedToCreateDropdownButton = this.$wrap.find('.js-allowed-to-create');
|
||||
this.onSelectCallback = this.onSelect.bind(this);
|
||||
|
||||
this.buildDropdowns();
|
||||
}
|
||||
|
||||
buildDropdowns() {
|
||||
// Allowed to create dropdown
|
||||
this.protectedTagAccessDropdown = new ProtectedTagAccessDropdown({
|
||||
$dropdown: this.$allowedToCreateDropdownButton,
|
||||
data: gon.create_access_levels,
|
||||
onSelect: this.onSelectCallback,
|
||||
});
|
||||
}
|
||||
|
||||
onSelect() {
|
||||
const $allowedToCreateInput = this.$wrap.find(`input[name="${this.$allowedToCreateDropdownButton.data('fieldName')}"]`);
|
||||
|
||||
// Do not update if one dropdown has not selected any option
|
||||
if (!$allowedToCreateInput.length) return;
|
||||
|
||||
this.$allowedToCreateDropdownButton.disable();
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: this.$wrap.data('url'),
|
||||
dataType: 'json',
|
||||
data: {
|
||||
_method: 'PATCH',
|
||||
protected_tag: {
|
||||
create_access_levels_attributes: [{
|
||||
id: this.$allowedToCreateDropdownButton.data('access-level-id'),
|
||||
access_level: $allowedToCreateInput.val(),
|
||||
}],
|
||||
},
|
||||
},
|
||||
error() {
|
||||
new Flash('Failed to update tag!', null, $('.js-protected-tags-list'));
|
||||
},
|
||||
}).always(() => {
|
||||
this.$allowedToCreateDropdownButton.enable();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/* eslint-disable no-new */
|
||||
|
||||
import ProtectedTagEdit from './protected_tag_edit';
|
||||
|
||||
export default class ProtectedTagEditList {
|
||||
constructor() {
|
||||
this.$wrap = $('.protected-tags-list');
|
||||
this.initEditForm();
|
||||
}
|
||||
|
||||
initEditForm() {
|
||||
this.$wrap.find('.js-protected-tag-edit-form').each((i, el) => {
|
||||
new ProtectedTagEdit({
|
||||
$wrap: $(el),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -744,7 +744,8 @@ pre.light-well {
|
|||
text-align: left;
|
||||
}
|
||||
|
||||
.protected-branches-list {
|
||||
.protected-branches-list,
|
||||
.protected-tags-list {
|
||||
margin-bottom: 30px;
|
||||
|
||||
a {
|
||||
|
@ -776,6 +777,17 @@ pre.light-well {
|
|||
}
|
||||
}
|
||||
|
||||
.protected-tags-list {
|
||||
.dropdown-menu-toggle {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.flash-container {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-notifications-form {
|
||||
.is-loading {
|
||||
.custom-notification-event-loading {
|
||||
|
|
|
@ -1,58 +1,23 @@
|
|||
class Projects::ProtectedBranchesController < Projects::ApplicationController
|
||||
include RepositorySettingsRedirect
|
||||
# Authorize
|
||||
before_action :require_non_empty_project
|
||||
before_action :authorize_admin_project!
|
||||
before_action :load_protected_branch, only: [:show, :update, :destroy]
|
||||
class Projects::ProtectedBranchesController < Projects::ProtectedRefsController
|
||||
protected
|
||||
|
||||
layout "project_settings"
|
||||
|
||||
def index
|
||||
redirect_to_repository_settings(@project)
|
||||
def project_refs
|
||||
@project.repository.branches
|
||||
end
|
||||
|
||||
def create
|
||||
@protected_branch = ::ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute
|
||||
unless @protected_branch.persisted?
|
||||
flash[:alert] = @protected_branches.errors.full_messages.join(', ').html_safe
|
||||
end
|
||||
redirect_to_repository_settings(@project)
|
||||
def create_service_class
|
||||
::ProtectedBranches::CreateService
|
||||
end
|
||||
|
||||
def show
|
||||
@matching_branches = @protected_branch.matching(@project.repository.branches)
|
||||
def update_service_class
|
||||
::ProtectedBranches::UpdateService
|
||||
end
|
||||
|
||||
def update
|
||||
@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
|
||||
else
|
||||
respond_to do |format|
|
||||
format.json { render json: @protected_branch.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
def load_protected_ref
|
||||
@protected_ref = @project.protected_branches.find(params[:id])
|
||||
end
|
||||
|
||||
def destroy
|
||||
@protected_branch.destroy
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to_repository_settings(@project) }
|
||||
format.js { head :ok }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_protected_branch
|
||||
@protected_branch = @project.protected_branches.find(params[:id])
|
||||
end
|
||||
|
||||
def protected_branch_params
|
||||
def protected_ref_params
|
||||
params.require(:protected_branch).permit(:name,
|
||||
merge_access_levels_attributes: [:access_level, :id],
|
||||
push_access_levels_attributes: [:access_level, :id])
|
||||
|
|
47
app/controllers/projects/protected_refs_controller.rb
Normal file
47
app/controllers/projects/protected_refs_controller.rb
Normal file
|
@ -0,0 +1,47 @@
|
|||
class Projects::ProtectedRefsController < Projects::ApplicationController
|
||||
include RepositorySettingsRedirect
|
||||
|
||||
# Authorize
|
||||
before_action :require_non_empty_project
|
||||
before_action :authorize_admin_project!
|
||||
before_action :load_protected_ref, only: [:show, :update, :destroy]
|
||||
|
||||
layout "project_settings"
|
||||
|
||||
def index
|
||||
redirect_to_repository_settings(@project)
|
||||
end
|
||||
|
||||
def create
|
||||
protected_ref = create_service_class.new(@project, current_user, protected_ref_params).execute
|
||||
|
||||
unless protected_ref.persisted?
|
||||
flash[:alert] = protected_ref.errors.full_messages.join(', ').html_safe
|
||||
end
|
||||
|
||||
redirect_to_repository_settings(@project)
|
||||
end
|
||||
|
||||
def show
|
||||
@matching_refs = @protected_ref.matching(project_refs)
|
||||
end
|
||||
|
||||
def update
|
||||
@protected_ref = update_service_class.new(@project, current_user, protected_ref_params).execute(@protected_ref)
|
||||
|
||||
if @protected_ref.valid?
|
||||
render json: @protected_ref, status: :ok
|
||||
else
|
||||
render json: @protected_ref.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@protected_ref.destroy
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to_repository_settings(@project) }
|
||||
format.js { head :ok }
|
||||
end
|
||||
end
|
||||
end
|
23
app/controllers/projects/protected_tags_controller.rb
Normal file
23
app/controllers/projects/protected_tags_controller.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
class Projects::ProtectedTagsController < Projects::ProtectedRefsController
|
||||
protected
|
||||
|
||||
def project_refs
|
||||
@project.repository.tags
|
||||
end
|
||||
|
||||
def create_service_class
|
||||
::ProtectedTags::CreateService
|
||||
end
|
||||
|
||||
def update_service_class
|
||||
::ProtectedTags::UpdateService
|
||||
end
|
||||
|
||||
def load_protected_ref
|
||||
@protected_ref = @project.protected_tags.find(params[:id])
|
||||
end
|
||||
|
||||
def protected_ref_params
|
||||
params.require(:protected_tag).permit(:name, create_access_levels_attributes: [:access_level, :id])
|
||||
end
|
||||
end
|
|
@ -4,46 +4,48 @@ module Projects
|
|||
before_action :authorize_admin_project!
|
||||
|
||||
def show
|
||||
@deploy_keys = DeployKeysPresenter
|
||||
.new(@project, current_user: current_user)
|
||||
@deploy_keys = DeployKeysPresenter.new(@project, current_user: current_user)
|
||||
|
||||
define_protected_branches
|
||||
define_protected_refs
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def define_protected_branches
|
||||
load_protected_branches
|
||||
@protected_branch = @project.protected_branches.new
|
||||
load_gon_index
|
||||
end
|
||||
|
||||
def load_protected_branches
|
||||
def define_protected_refs
|
||||
@protected_branches = @project.protected_branches.order(:name).page(params[:page])
|
||||
@protected_tags = @project.protected_tags.order(:name).page(params[:page])
|
||||
@protected_branch = @project.protected_branches.new
|
||||
@protected_tag = @project.protected_tags.new
|
||||
load_gon_index
|
||||
end
|
||||
|
||||
def access_levels_options
|
||||
{
|
||||
push_access_levels: {
|
||||
roles: ProtectedBranch::PushAccessLevel.human_access_levels.map do |id, text|
|
||||
{ id: id, text: text, before_divider: true }
|
||||
end
|
||||
},
|
||||
merge_access_levels: {
|
||||
roles: ProtectedBranch::MergeAccessLevel.human_access_levels.map do |id, text|
|
||||
{ id: id, text: text, before_divider: true }
|
||||
end
|
||||
}
|
||||
create_access_levels: levels_for_dropdown(ProtectedTag::CreateAccessLevel),
|
||||
push_access_levels: levels_for_dropdown(ProtectedBranch::PushAccessLevel),
|
||||
merge_access_levels: levels_for_dropdown(ProtectedBranch::MergeAccessLevel)
|
||||
}
|
||||
end
|
||||
|
||||
def open_branches
|
||||
branches = @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } }
|
||||
{ open_branches: branches }
|
||||
def levels_for_dropdown(access_level_type)
|
||||
roles = access_level_type.human_access_levels.map do |id, text|
|
||||
{ id: id, text: text, before_divider: true }
|
||||
end
|
||||
{ roles: roles }
|
||||
end
|
||||
|
||||
def protectable_tags_for_dropdown
|
||||
{ open_tags: ProtectableDropdown.new(@project, :tags).hash }
|
||||
end
|
||||
|
||||
def protectable_branches_for_dropdown
|
||||
{ open_branches: ProtectableDropdown.new(@project, :branches).hash }
|
||||
end
|
||||
|
||||
def load_gon_index
|
||||
gon.push(open_branches.merge(access_levels_options))
|
||||
gon.push(protectable_tags_for_dropdown)
|
||||
gon.push(protectable_branches_for_dropdown)
|
||||
gon.push(access_levels_options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
module BranchesHelper
|
||||
def can_remove_branch?(project, branch_name)
|
||||
if project.protected_branch? branch_name
|
||||
if ProtectedBranch.protected?(project, branch_name)
|
||||
false
|
||||
elsif branch_name == project.repository.root_ref
|
||||
false
|
||||
|
@ -29,4 +29,8 @@ module BranchesHelper
|
|||
def project_branches
|
||||
options_for_select(@project.repository.branch_names, @project.default_branch)
|
||||
end
|
||||
|
||||
def protected_branch?(project, branch)
|
||||
ProtectedBranch.protected?(project, branch.name)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,4 +21,8 @@ module TagsHelper
|
|||
|
||||
html.html_safe
|
||||
end
|
||||
|
||||
def protected_tag?(project, tag)
|
||||
ProtectedTag.protected?(project, tag.name)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,20 +2,10 @@ module ProtectedBranchAccess
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
include ProtectedRefAccess
|
||||
|
||||
belongs_to :protected_branch
|
||||
|
||||
delegate :project, to: :protected_branch
|
||||
|
||||
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
|
||||
|
||||
def check_access(user)
|
||||
return true if user.is_admin?
|
||||
|
||||
project.team.max_member_access(user.id) >= access_level
|
||||
end
|
||||
end
|
||||
|
|
42
app/models/concerns/protected_ref.rb
Normal file
42
app/models/concerns/protected_ref.rb
Normal file
|
@ -0,0 +1,42 @@
|
|||
module ProtectedRef
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
belongs_to :project
|
||||
|
||||
validates :name, presence: true
|
||||
validates :project, presence: true
|
||||
|
||||
delegate :matching, :matches?, :wildcard?, to: :ref_matcher
|
||||
|
||||
def self.protected_ref_accessible_to?(ref, user, action:)
|
||||
access_levels_for_ref(ref, action: action).any? do |access_level|
|
||||
access_level.check_access(user)
|
||||
end
|
||||
end
|
||||
|
||||
def self.developers_can?(action, ref)
|
||||
access_levels_for_ref(ref, action: action).any? do |access_level|
|
||||
access_level.access_level == Gitlab::Access::DEVELOPER
|
||||
end
|
||||
end
|
||||
|
||||
def self.access_levels_for_ref(ref, action:)
|
||||
self.matching(ref).map(&:"#{action}_access_levels").flatten
|
||||
end
|
||||
|
||||
def self.matching(ref_name, protected_refs: nil)
|
||||
ProtectedRefMatcher.matching(self, ref_name, protected_refs: protected_refs)
|
||||
end
|
||||
end
|
||||
|
||||
def commit
|
||||
project.commit(self.name)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ref_matcher
|
||||
@ref_matcher ||= ProtectedRefMatcher.new(self)
|
||||
end
|
||||
end
|
18
app/models/concerns/protected_ref_access.rb
Normal file
18
app/models/concerns/protected_ref_access.rb
Normal file
|
@ -0,0 +1,18 @@
|
|||
module ProtectedRefAccess
|
||||
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
|
||||
|
||||
def check_access(user)
|
||||
return true if user.admin?
|
||||
|
||||
project.team.max_member_access(user.id) >= access_level
|
||||
end
|
||||
end
|
11
app/models/concerns/protected_tag_access.rb
Normal file
11
app/models/concerns/protected_tag_access.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
module ProtectedTagAccess
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
include ProtectedRefAccess
|
||||
|
||||
belongs_to :protected_tag
|
||||
|
||||
delegate :project, to: :protected_tag
|
||||
end
|
||||
end
|
|
@ -442,7 +442,7 @@ class MergeRequest < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def can_remove_source_branch?(current_user)
|
||||
!source_project.protected_branch?(source_branch) &&
|
||||
!ProtectedBranch.protected?(source_project, source_branch) &&
|
||||
!source_project.root_ref?(source_branch) &&
|
||||
Ability.allowed?(current_user, :push_code, source_project) &&
|
||||
diff_head_commit == source_branch_head
|
||||
|
|
|
@ -135,6 +135,7 @@ class Project < ActiveRecord::Base
|
|||
has_many :snippets, dependent: :destroy, class_name: 'ProjectSnippet'
|
||||
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
|
||||
has_many :protected_branches, dependent: :destroy
|
||||
has_many :protected_tags, dependent: :destroy
|
||||
|
||||
has_many :project_authorizations
|
||||
has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
|
||||
|
@ -859,14 +860,6 @@ class Project < ActiveRecord::Base
|
|||
@repo_exists = false
|
||||
end
|
||||
|
||||
# Branches that are not _exactly_ matched by a protected branch.
|
||||
def open_branches
|
||||
exact_protected_branch_names = protected_branches.reject(&:wildcard?).map(&:name)
|
||||
branch_names = repository.branches.map(&:name)
|
||||
non_open_branch_names = Set.new(exact_protected_branch_names).intersection(Set.new(branch_names))
|
||||
repository.branches.reject { |branch| non_open_branch_names.include? branch.name }
|
||||
end
|
||||
|
||||
def root_ref?(branch)
|
||||
repository.root_ref == branch
|
||||
end
|
||||
|
@ -881,16 +874,8 @@ class Project < ActiveRecord::Base
|
|||
Gitlab::UrlSanitizer.new("#{web_url}.git", credentials: credentials).full_url
|
||||
end
|
||||
|
||||
# Check if current branch name is marked as protected in the system
|
||||
def protected_branch?(branch_name)
|
||||
return true if empty_repo? && default_branch_protected?
|
||||
|
||||
@protected_branches ||= self.protected_branches.to_a
|
||||
ProtectedBranch.matching(branch_name, protected_branches: @protected_branches).present?
|
||||
end
|
||||
|
||||
def user_can_push_to_empty_repo?(user)
|
||||
!default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
|
||||
!ProtectedBranch.default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
|
||||
end
|
||||
|
||||
def forked?
|
||||
|
@ -1353,11 +1338,6 @@ class Project < ActiveRecord::Base
|
|||
"projects/#{id}/pushes_since_gc"
|
||||
end
|
||||
|
||||
def default_branch_protected?
|
||||
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
|
||||
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
|
||||
end
|
||||
|
||||
# Similar to the normal callbacks that hook into the life cycle of an
|
||||
# Active Record object, you can also define callbacks that get triggered
|
||||
# when you add an object to an association collection. If any of these
|
||||
|
|
33
app/models/protectable_dropdown.rb
Normal file
33
app/models/protectable_dropdown.rb
Normal file
|
@ -0,0 +1,33 @@
|
|||
class ProtectableDropdown
|
||||
def initialize(project, ref_type)
|
||||
@project = project
|
||||
@ref_type = ref_type
|
||||
end
|
||||
|
||||
# Tags/branches which are yet to be individually protected
|
||||
def protectable_ref_names
|
||||
@protectable_ref_names ||= ref_names - non_wildcard_protected_ref_names
|
||||
end
|
||||
|
||||
def hash
|
||||
protectable_ref_names.map { |ref_name| { text: ref_name, id: ref_name, title: ref_name } }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def refs
|
||||
@project.repository.public_send(@ref_type)
|
||||
end
|
||||
|
||||
def ref_names
|
||||
refs.map(&:name)
|
||||
end
|
||||
|
||||
def protections
|
||||
@project.public_send("protected_#{@ref_type}")
|
||||
end
|
||||
|
||||
def non_wildcard_protected_ref_names
|
||||
protections.reject(&:wildcard?).map(&:name)
|
||||
end
|
||||
end
|
|
@ -1,9 +1,6 @@
|
|||
class ProtectedBranch < ActiveRecord::Base
|
||||
include Gitlab::ShellAdapter
|
||||
|
||||
belongs_to :project
|
||||
validates :name, presence: true
|
||||
validates :project, presence: true
|
||||
include ProtectedRef
|
||||
|
||||
has_many :merge_access_levels, dependent: :destroy
|
||||
has_many :push_access_levels, dependent: :destroy
|
||||
|
@ -14,54 +11,15 @@ class ProtectedBranch < ActiveRecord::Base
|
|||
accepts_nested_attributes_for :push_access_levels
|
||||
accepts_nested_attributes_for :merge_access_levels
|
||||
|
||||
def commit
|
||||
project.commit(self.name)
|
||||
# Check if branch name is marked as protected in the system
|
||||
def self.protected?(project, ref_name)
|
||||
return true if project.empty_repo? && default_branch_protected?
|
||||
|
||||
self.matching(ref_name, protected_refs: project.protected_branches).present?
|
||||
end
|
||||
|
||||
# Returns all protected branches that match the given branch name.
|
||||
# This realizes all records from the scope built up so far, and does
|
||||
# _not_ return a relation.
|
||||
#
|
||||
# This method optionally takes in a list of `protected_branches` to search
|
||||
# through, to avoid calling out to the database.
|
||||
def self.matching(branch_name, protected_branches: nil)
|
||||
(protected_branches || all).select { |protected_branch| protected_branch.matches?(branch_name) }
|
||||
end
|
||||
|
||||
# Returns all branches (among the given list of branches [`Gitlab::Git::Branch`])
|
||||
# that match the current protected branch.
|
||||
def matching(branches)
|
||||
branches.select { |branch| self.matches?(branch.name) }
|
||||
end
|
||||
|
||||
# Checks if the protected branch matches the given branch name.
|
||||
def matches?(branch_name)
|
||||
return false if self.name.blank?
|
||||
|
||||
exact_match?(branch_name) || wildcard_match?(branch_name)
|
||||
end
|
||||
|
||||
# Checks if this protected branch contains a wildcard
|
||||
def wildcard?
|
||||
self.name && self.name.include?('*')
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def exact_match?(branch_name)
|
||||
self.name == branch_name
|
||||
end
|
||||
|
||||
def wildcard_match?(branch_name)
|
||||
wildcard_regex === branch_name
|
||||
end
|
||||
|
||||
def wildcard_regex
|
||||
@wildcard_regex ||= begin
|
||||
name = self.name.gsub('*', 'STAR_DONT_ESCAPE')
|
||||
quoted_name = Regexp.quote(name)
|
||||
regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?')
|
||||
/\A#{regex_string}\z/
|
||||
end
|
||||
def self.default_branch_protected?
|
||||
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
|
||||
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
|
||||
end
|
||||
end
|
||||
|
|
54
app/models/protected_ref_matcher.rb
Normal file
54
app/models/protected_ref_matcher.rb
Normal file
|
@ -0,0 +1,54 @@
|
|||
class ProtectedRefMatcher
|
||||
def initialize(protected_ref)
|
||||
@protected_ref = protected_ref
|
||||
end
|
||||
|
||||
# Returns all protected refs that match the given ref name.
|
||||
# This checks all records from the scope built up so far, and does
|
||||
# _not_ return a relation.
|
||||
#
|
||||
# This method optionally takes in a list of `protected_refs` to search
|
||||
# through, to avoid calling out to the database.
|
||||
def self.matching(type, ref_name, protected_refs: nil)
|
||||
(protected_refs || type.all).select { |protected_ref| protected_ref.matches?(ref_name) }
|
||||
end
|
||||
|
||||
# Returns all branches/tags (among the given list of refs [`Gitlab::Git::Branch`])
|
||||
# that match the current protected ref.
|
||||
def matching(refs)
|
||||
refs.select { |ref| @protected_ref.matches?(ref.name) }
|
||||
end
|
||||
|
||||
# Checks if the protected ref matches the given ref name.
|
||||
def matches?(ref_name)
|
||||
return false if @protected_ref.name.blank?
|
||||
|
||||
exact_match?(ref_name) || wildcard_match?(ref_name)
|
||||
end
|
||||
|
||||
# Checks if this protected ref contains a wildcard
|
||||
def wildcard?
|
||||
@protected_ref.name && @protected_ref.name.include?('*')
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def exact_match?(ref_name)
|
||||
@protected_ref.name == ref_name
|
||||
end
|
||||
|
||||
def wildcard_match?(ref_name)
|
||||
return false unless wildcard?
|
||||
|
||||
wildcard_regex === ref_name
|
||||
end
|
||||
|
||||
def wildcard_regex
|
||||
@wildcard_regex ||= begin
|
||||
name = @protected_ref.name.gsub('*', 'STAR_DONT_ESCAPE')
|
||||
quoted_name = Regexp.quote(name)
|
||||
regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?')
|
||||
/\A#{regex_string}\z/
|
||||
end
|
||||
end
|
||||
end
|
14
app/models/protected_tag.rb
Normal file
14
app/models/protected_tag.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
class ProtectedTag < ActiveRecord::Base
|
||||
include Gitlab::ShellAdapter
|
||||
include ProtectedRef
|
||||
|
||||
has_many :create_access_levels, dependent: :destroy
|
||||
|
||||
validates :create_access_levels, length: { is: 1, message: "are restricted to a single instance per protected tag." }
|
||||
|
||||
accepts_nested_attributes_for :create_access_levels
|
||||
|
||||
def self.protected?(project, ref_name)
|
||||
self.matching(ref_name, protected_refs: project.protected_tags).present?
|
||||
end
|
||||
end
|
21
app/models/protected_tag/create_access_level.rb
Normal file
21
app/models/protected_tag/create_access_level.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
class ProtectedTag::CreateAccessLevel < ActiveRecord::Base
|
||||
include ProtectedTagAccess
|
||||
|
||||
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
|
||||
|
||||
super
|
||||
end
|
||||
end
|
|
@ -11,7 +11,7 @@ class DeleteBranchService < BaseService
|
|||
return error('Cannot remove HEAD branch', 405)
|
||||
end
|
||||
|
||||
if project.protected_branch?(branch_name)
|
||||
if ProtectedBranch.protected?(project, branch_name)
|
||||
return error('Protected branch cant be removed', 405)
|
||||
end
|
||||
|
||||
|
|
|
@ -127,7 +127,7 @@ class GitPushService < BaseService
|
|||
project.change_head(branch_name)
|
||||
|
||||
# Set protection on the default branch if configured
|
||||
if current_application_settings.default_branch_protection != PROTECTION_NONE && !@project.protected_branch?(@project.default_branch)
|
||||
if current_application_settings.default_branch_protection != PROTECTION_NONE && !ProtectedBranch.protected?(@project, @project.default_branch)
|
||||
|
||||
params = {
|
||||
name: @project.default_branch,
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
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
|
||||
protected_branch.update(params)
|
||||
protected_branch
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
11
app/services/protected_tags/create_service.rb
Normal file
11
app/services/protected_tags/create_service.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
module ProtectedTags
|
||||
class CreateService < BaseService
|
||||
attr_reader :protected_tag
|
||||
|
||||
def execute
|
||||
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
|
||||
|
||||
project.protected_tags.create(params)
|
||||
end
|
||||
end
|
||||
end
|
10
app/services/protected_tags/update_service.rb
Normal file
10
app/services/protected_tags/update_service.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
module ProtectedTags
|
||||
class UpdateService < BaseService
|
||||
def execute(protected_tag)
|
||||
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
|
||||
|
||||
protected_tag.update(params)
|
||||
protected_tag
|
||||
end
|
||||
end
|
||||
end
|
|
@ -15,7 +15,7 @@
|
|||
%span.label.label-info.has-tooltip{ title: "Merged into #{@repository.root_ref}" }
|
||||
merged
|
||||
|
||||
- if @project.protected_branch? branch.name
|
||||
- if protected_branch?(@project, branch)
|
||||
%span.label.label-success
|
||||
protected
|
||||
.controls.hidden-xs<
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
- page_title @protected_branch.name, "Protected Branches"
|
||||
- page_title @protected_ref.name, "Protected Branches"
|
||||
|
||||
.row.prepend-top-default.append-bottom-default
|
||||
.col-lg-3
|
||||
%h4.prepend-top-0
|
||||
= @protected_branch.name
|
||||
= @protected_ref.name
|
||||
|
||||
.col-lg-9
|
||||
%h5 Matching Branches
|
||||
- if @matching_branches.present?
|
||||
- if @matching_refs.present?
|
||||
.table-responsive
|
||||
%table.table.protected-branches-list
|
||||
%colgroup
|
||||
|
@ -18,7 +18,7 @@
|
|||
%th Branch
|
||||
%th Last commit
|
||||
%tbody
|
||||
- @matching_branches.each do |matching_branch|
|
||||
- @matching_refs.each do |matching_branch|
|
||||
= render partial: "matching_branch", object: matching_branch
|
||||
- else
|
||||
%p.settings-message.text-center
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
= form_for [@project.namespace.becomes(Namespace), @project, @protected_tag], html: { class: 'js-new-protected-tag' } do |f|
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
%h3.panel-title
|
||||
Protect a tag
|
||||
.panel-body
|
||||
.form-horizontal
|
||||
= form_errors(@protected_tag)
|
||||
.form-group
|
||||
= f.label :name, class: 'col-md-2 text-right' do
|
||||
Tag:
|
||||
.col-md-10
|
||||
= render partial: "projects/protected_tags/dropdown", locals: { f: f }
|
||||
.help-block
|
||||
= link_to 'Wildcards', help_page_path('user/project/protected_tags', anchor: 'wildcard-protected-tags')
|
||||
such as
|
||||
%code v*
|
||||
or
|
||||
%code *-release
|
||||
are supported
|
||||
.form-group
|
||||
%label.col-md-2.text-right{ for: 'create_access_levels_attributes' }
|
||||
Allowed to create:
|
||||
.col-md-10
|
||||
.create_access_levels-container
|
||||
= dropdown_tag('Select',
|
||||
options: { toggle_class: 'js-allowed-to-create wide',
|
||||
dropdown_class: 'dropdown-menu-selectable',
|
||||
data: { field_name: 'protected_tag[create_access_levels_attributes][0][access_level]', input_id: 'create_access_levels_attributes' }})
|
||||
|
||||
.panel-footer
|
||||
= f.submit 'Protect', class: 'btn-create btn', disabled: true
|
15
app/views/projects/protected_tags/_dropdown.html.haml
Normal file
15
app/views/projects/protected_tags/_dropdown.html.haml
Normal file
|
@ -0,0 +1,15 @@
|
|||
= f.hidden_field(:name)
|
||||
|
||||
= dropdown_tag('Select tag or create wildcard',
|
||||
options: { toggle_class: 'js-protected-tag-select js-filter-submit wide',
|
||||
filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search protected tag",
|
||||
footer_content: true,
|
||||
data: { show_no: true, show_any: true, show_upcoming: true,
|
||||
selected: params[:protected_tag_name],
|
||||
project_id: @project.try(:id) } }) do
|
||||
|
||||
%ul.dropdown-footer-list
|
||||
%li
|
||||
= link_to '#', title: "New Protected Tag", class: "create-new-protected-tag" do
|
||||
Create wildcard
|
||||
%code
|
18
app/views/projects/protected_tags/_index.html.haml
Normal file
18
app/views/projects/protected_tags/_index.html.haml
Normal file
|
@ -0,0 +1,18 @@
|
|||
- content_for :page_specific_javascripts do
|
||||
= page_specific_javascript_bundle_tag('protected_tags')
|
||||
|
||||
.row.prepend-top-default.append-bottom-default
|
||||
.col-lg-3
|
||||
%h4.prepend-top-0
|
||||
Protected tags
|
||||
%p.prepend-top-20
|
||||
By default, Protected tags are designed to:
|
||||
%ul
|
||||
%li Prevent tag creation by everybody except Masters
|
||||
%li Prevent <strong>anyone</strong> from updating the tag
|
||||
%li Prevent <strong>anyone</strong> from deleting the tag
|
||||
.col-lg-9
|
||||
- if can? current_user, :admin_project, @project
|
||||
= render 'projects/protected_tags/create_protected_tag'
|
||||
|
||||
= render "projects/protected_tags/tags_list"
|
|
@ -0,0 +1,9 @@
|
|||
%tr
|
||||
%td
|
||||
= link_to matching_tag.name, namespace_project_tree_path(@project.namespace, @project, matching_tag.name)
|
||||
- if @project.root_ref?(matching_tag.name)
|
||||
%span.label.label-info.prepend-left-5 default
|
||||
%td
|
||||
- commit = @project.commit(matching_tag.name)
|
||||
= link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id')
|
||||
= time_ago_with_tooltip(commit.committed_date)
|
21
app/views/projects/protected_tags/_protected_tag.html.haml
Normal file
21
app/views/projects/protected_tags/_protected_tag.html.haml
Normal file
|
@ -0,0 +1,21 @@
|
|||
%tr.js-protected-tag-edit-form{ data: { url: namespace_project_protected_tag_path(@project.namespace, @project, protected_tag) } }
|
||||
%td
|
||||
= protected_tag.name
|
||||
- if @project.root_ref?(protected_tag.name)
|
||||
%span.label.label-info.prepend-left-5 default
|
||||
%td
|
||||
- if protected_tag.wildcard?
|
||||
- matching_tags = protected_tag.matching(repository.tags)
|
||||
= link_to pluralize(matching_tags.count, "matching tag"), namespace_project_protected_tag_path(@project.namespace, @project, protected_tag)
|
||||
- else
|
||||
- if commit = protected_tag.commit
|
||||
= link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id')
|
||||
= time_ago_with_tooltip(commit.committed_date)
|
||||
- else
|
||||
(tag was removed from repository)
|
||||
|
||||
= render partial: 'projects/protected_tags/update_protected_tag', locals: { protected_tag: protected_tag }
|
||||
|
||||
- if can_admin_project
|
||||
%td
|
||||
= link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_tag], data: { confirm: 'tag will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning'
|
28
app/views/projects/protected_tags/_tags_list.html.haml
Normal file
28
app/views/projects/protected_tags/_tags_list.html.haml
Normal file
|
@ -0,0 +1,28 @@
|
|||
.panel.panel-default.protected-tags-list.js-protected-tags-list
|
||||
- if @protected_tags.empty?
|
||||
.panel-heading
|
||||
%h3.panel-title
|
||||
Protected tag (#{@protected_tags.size})
|
||||
%p.settings-message.text-center
|
||||
There are currently no protected tags, protect a tag with the form above.
|
||||
- else
|
||||
- can_admin_project = can?(current_user, :admin_project, @project)
|
||||
|
||||
%table.table.table-bordered
|
||||
%colgroup
|
||||
%col{ width: "25%" }
|
||||
%col{ width: "25%" }
|
||||
%col{ width: "50%" }
|
||||
%thead
|
||||
%tr
|
||||
%th Protected tag (#{@protected_tags.size})
|
||||
%th Last commit
|
||||
%th Allowed to create
|
||||
- if can_admin_project
|
||||
%th
|
||||
%tbody
|
||||
%tr
|
||||
%td.flash-container{ colspan: 4 }
|
||||
= render partial: 'projects/protected_tags/protected_tag', collection: @protected_tags, locals: { can_admin_project: can_admin_project}
|
||||
|
||||
= paginate @protected_tags, theme: 'gitlab'
|
|
@ -0,0 +1,5 @@
|
|||
%td
|
||||
= hidden_field_tag "allowed_to_create_#{protected_tag.id}", protected_tag.create_access_levels.first.access_level
|
||||
= dropdown_tag( (protected_tag.create_access_levels.first.humanize || 'Select') ,
|
||||
options: { toggle_class: 'js-allowed-to-create', dropdown_class: 'dropdown-menu-selectable js-allowed-to-create-container',
|
||||
data: { field_name: "allowed_to_create_#{protected_tag.id}", access_level_id: protected_tag.create_access_levels.first.id }})
|
25
app/views/projects/protected_tags/show.html.haml
Normal file
25
app/views/projects/protected_tags/show.html.haml
Normal file
|
@ -0,0 +1,25 @@
|
|||
- page_title @protected_ref.name, "Protected Tags"
|
||||
|
||||
.row.prepend-top-default.append-bottom-default
|
||||
.col-lg-3
|
||||
%h4.prepend-top-0
|
||||
= @protected_ref.name
|
||||
|
||||
.col-lg-9
|
||||
%h5 Matching Tags
|
||||
- if @matching_refs.present?
|
||||
.table-responsive
|
||||
%table.table.protected-tags-list
|
||||
%colgroup
|
||||
%col{ width: "30%" }
|
||||
%col{ width: "30%" }
|
||||
%thead
|
||||
%tr
|
||||
%th Tag
|
||||
%th Last commit
|
||||
%tbody
|
||||
- @matching_refs.each do |matching_tag|
|
||||
= render partial: "matching_tag", object: matching_tag
|
||||
- else
|
||||
%p.settings-message.text-center
|
||||
Couldn't find any matching tags.
|
|
@ -3,3 +3,4 @@
|
|||
|
||||
= render @deploy_keys
|
||||
= render "projects/protected_branches/index"
|
||||
= render "projects/protected_tags/index"
|
||||
|
|
|
@ -6,6 +6,11 @@
|
|||
%span.item-title
|
||||
= icon('tag')
|
||||
= tag.name
|
||||
|
||||
- if protected_tag?(@project, tag)
|
||||
%span.label.label-success
|
||||
protected
|
||||
|
||||
- if tag.message.present?
|
||||
|
||||
= strip_gpg_signature(tag.message)
|
||||
|
@ -30,5 +35,5 @@
|
|||
= icon("pencil")
|
||||
|
||||
- if can?(current_user, :admin_project, @project)
|
||||
= link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do
|
||||
= link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do
|
||||
= icon("trash-o")
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
.nav-text
|
||||
.title
|
||||
%span.item-title= @tag.name
|
||||
- if protected_tag?(@project, @tag)
|
||||
%span.label.label-success
|
||||
protected
|
||||
- if @commit
|
||||
= render 'projects/branches/commit', commit: @commit, project: @project
|
||||
- else
|
||||
|
@ -24,7 +27,7 @@
|
|||
= render 'projects/buttons/download', project: @project, ref: @tag.name
|
||||
- if can?(current_user, :admin_project, @project)
|
||||
.btn-container.controls-item-full
|
||||
= link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
|
||||
= link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, @tag) ? 'disabled' : ''}", title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
|
||||
%i.fa.fa-trash-o
|
||||
|
||||
- if @tag.message.present?
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Tags can be protected, restricting creation of matching tags by user role
|
||||
merge_request: 10356
|
||||
author:
|
|
@ -135,6 +135,8 @@ constraints(ProjectUrlConstrainer.new) do
|
|||
end
|
||||
|
||||
resources :protected_branches, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
|
||||
resources :protected_tags, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
|
||||
|
||||
resources :variables, only: [:index, :show, :update, :create, :destroy]
|
||||
resources :triggers, only: [:index, :create, :edit, :update, :destroy] do
|
||||
member do
|
||||
|
|
|
@ -41,6 +41,7 @@ var config = {
|
|||
pdf_viewer: './blob/pdf_viewer.js',
|
||||
profile: './profile/profile_bundle.js',
|
||||
protected_branches: './protected_branches/protected_branches_bundle.js',
|
||||
protected_tags: './protected_tags',
|
||||
snippet: './snippet/snippet_bundle.js',
|
||||
stl_viewer: './blob/stl_viewer.js',
|
||||
terminal: './terminal/terminal_bundle.js',
|
||||
|
|
27
db/migrate/20170309173138_create_protected_tags.rb
Normal file
27
db/migrate/20170309173138_create_protected_tags.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
class CreateProtectedTags < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
GITLAB_ACCESS_MASTER = 40
|
||||
|
||||
def change
|
||||
create_table :protected_tags do |t|
|
||||
t.integer :project_id, null: false
|
||||
t.string :name, null: false
|
||||
t.timestamps null: false
|
||||
end
|
||||
|
||||
add_index :protected_tags, :project_id
|
||||
|
||||
create_table :protected_tag_create_access_levels do |t|
|
||||
t.references :protected_tag, index: { name: "index_protected_tag_create_access" }, foreign_key: true, null: false
|
||||
t.integer :access_level, default: GITLAB_ACCESS_MASTER, null: true
|
||||
t.references :user, foreign_key: true, index: true
|
||||
t.integer :group_id
|
||||
t.timestamps null: false
|
||||
end
|
||||
|
||||
add_foreign_key :protected_tag_create_access_levels, :namespaces, column: :group_id # rubocop: disable Migration/AddConcurrentForeignKey
|
||||
end
|
||||
end
|
24
db/schema.rb
24
db/schema.rb
|
@ -996,6 +996,27 @@ ActiveRecord::Schema.define(version: 20170406115029) do
|
|||
|
||||
add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree
|
||||
|
||||
create_table "protected_tag_create_access_levels", force: :cascade do |t|
|
||||
t.integer "protected_tag_id", null: false
|
||||
t.integer "access_level", default: 40
|
||||
t.integer "user_id"
|
||||
t.integer "group_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
add_index "protected_tag_create_access_levels", ["protected_tag_id"], name: "index_protected_tag_create_access", using: :btree
|
||||
add_index "protected_tag_create_access_levels", ["user_id"], name: "index_protected_tag_create_access_levels_on_user_id", using: :btree
|
||||
|
||||
create_table "protected_tags", force: :cascade do |t|
|
||||
t.integer "project_id", null: false
|
||||
t.string "name", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
add_index "protected_tags", ["project_id"], name: "index_protected_tags_on_project_id", using: :btree
|
||||
|
||||
create_table "releases", force: :cascade do |t|
|
||||
t.string "tag"
|
||||
t.text "description"
|
||||
|
@ -1353,6 +1374,9 @@ ActiveRecord::Schema.define(version: 20170406115029) do
|
|||
add_foreign_key "project_statistics", "projects", on_delete: :cascade
|
||||
add_foreign_key "protected_branch_merge_access_levels", "protected_branches"
|
||||
add_foreign_key "protected_branch_push_access_levels", "protected_branches"
|
||||
add_foreign_key "protected_tag_create_access_levels", "namespaces", column: "group_id"
|
||||
add_foreign_key "protected_tag_create_access_levels", "protected_tags"
|
||||
add_foreign_key "protected_tag_create_access_levels", "users"
|
||||
add_foreign_key "subscriptions", "projects", on_delete: :cascade
|
||||
add_foreign_key "system_note_metadata", "notes", name: "fk_d83a918cb1", on_delete: :cascade
|
||||
add_foreign_key "timelogs", "issues", name: "fk_timelogs_issues_issue_id", on_delete: :cascade
|
||||
|
|
|
@ -411,6 +411,10 @@ An [object-relational](https://en.wikipedia.org/wiki/PostgreSQL) database. Toute
|
|||
|
||||
A [feature](https://docs.gitlab.com/ce/user/project/protected_branches.html) that protects branches from unauthorized pushes, force pushing or deletion.
|
||||
|
||||
### Protected Tags
|
||||
|
||||
A [feature](https://docs.gitlab.com/ce/user/project/protected_tags.html) that protects tags from unauthorized creation, update or deletion
|
||||
|
||||
### Pull
|
||||
|
||||
Git command to [synchronize](https://git-scm.com/docs/git-pull) the local repository with the remote repository, by fetching all remote changes and merging them into the local repository.
|
||||
|
|
|
@ -55,6 +55,7 @@ The following table depicts the various user permission levels in a project.
|
|||
| Push to protected branches | | | | ✓ | ✓ |
|
||||
| Enable/disable branch protection | | | | ✓ | ✓ |
|
||||
| Turn on/off protected branch push for devs| | | | ✓ | ✓ |
|
||||
| Enable/disable tag protections | | | | ✓ | ✓ |
|
||||
| Rewrite/remove Git tags | | | | ✓ | ✓ |
|
||||
| Edit project | | | | ✓ | ✓ |
|
||||
| Add deploy keys to project | | | | ✓ | ✓ |
|
||||
|
|
BIN
doc/user/project/img/project_repository_settings.png
Normal file
BIN
doc/user/project/img/project_repository_settings.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
BIN
doc/user/project/img/protected_tag_matches.png
Normal file
BIN
doc/user/project/img/protected_tag_matches.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 83 KiB |
BIN
doc/user/project/img/protected_tags_list.png
Normal file
BIN
doc/user/project/img/protected_tags_list.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
BIN
doc/user/project/img/protected_tags_page.png
Normal file
BIN
doc/user/project/img/protected_tags_page.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 55 KiB |
BIN
doc/user/project/img/protected_tags_permissions_dropdown.png
Normal file
BIN
doc/user/project/img/protected_tags_permissions_dropdown.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
60
doc/user/project/protected_tags.md
Normal file
60
doc/user/project/protected_tags.md
Normal file
|
@ -0,0 +1,60 @@
|
|||
# Protected Tags
|
||||
|
||||
> [Introduced][ce-10356] in GitLab 9.1.
|
||||
|
||||
Protected Tags allow control over who has permission to create tags as well as preventing accidental update or deletion once created. Each rule allows you to match either an individual tag name, or use wildcards to control multiple tags at once.
|
||||
|
||||
This feature evolved out of [Protected Branches](protected_branches.md)
|
||||
|
||||
## Overview
|
||||
|
||||
Protected tags will prevent anyone from updating or deleting the tag, as and will prevent creation of matching tags based on the permissions you have selected. By default, anyone without Master permission will be prevented from creating tags.
|
||||
|
||||
|
||||
## Configuring protected tags
|
||||
|
||||
To protect a tag, you need to have at least Master permission level.
|
||||
|
||||
1. Navigate to the project's Settings -> Repository page
|
||||
|
||||
![Repository Settings](img/project_repository_settings.png)
|
||||
|
||||
1. From the **Tag** dropdown menu, select the tag you want to protect or type and click `Create wildcard`. In the screenshot below, we chose to protect all tags matching `v*`.
|
||||
|
||||
![Protected tags page](img/protected_tags_page.png)
|
||||
|
||||
1. From the `Allowed to create` dropdown, select who will have permission to create matching tags and then click `Protect`.
|
||||
|
||||
![Allowed to create tags dropdown](img/protected_tags_permissions_dropdown.png)
|
||||
|
||||
1. Once done, the protected tag will appear in the "Protected tags" list.
|
||||
|
||||
![Protected tags list](img/protected_tags_list.png)
|
||||
|
||||
## Wildcard protected tags
|
||||
|
||||
You can specify a wildcard protected tag, which will protect all tags
|
||||
matching the wildcard. For example:
|
||||
|
||||
| Wildcard Protected Tag | Matching Tags |
|
||||
|------------------------+-------------------------------|
|
||||
| `v*` | `v1.0.0`, `version-9.1` |
|
||||
| `*-deploy` | `march-deploy`, `1.0-deploy` |
|
||||
| `*gitlab*` | `gitlab`, `gitlab/v1` |
|
||||
| `*` | `v1.0.1rc2`, `accidental-tag` |
|
||||
|
||||
|
||||
Two different wildcards can potentially match the same tag. For example,
|
||||
`*-stable` and `production-*` would both match a `production-stable` tag.
|
||||
In that case, if _any_ of these protected tags have a setting like
|
||||
"Allowed to create", then `production-stable` will also inherit this setting.
|
||||
|
||||
If you click on a protected tag's name, you will be presented with a list of
|
||||
all matching tags:
|
||||
|
||||
![Protected tag matches](img/protected_tag_matches.png)
|
||||
|
||||
|
||||
---
|
||||
|
||||
[ce-10356]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10356 "Protected Tags"
|
|
@ -20,6 +20,7 @@
|
|||
- [Project forking workflow](forking_workflow.md)
|
||||
- [Project users](add-user/add-user.md)
|
||||
- [Protected branches](../user/project/protected_branches.md)
|
||||
- [Protected tags](../user/project/protected_tags.md)
|
||||
- [Slash commands](../user/project/slash_commands.md)
|
||||
- [Sharing a project with a group](share_with_group.md)
|
||||
- [Share projects with other groups](share_projects_with_other_groups.md)
|
||||
|
|
|
@ -184,19 +184,15 @@ module API
|
|||
end
|
||||
|
||||
expose :protected do |repo_branch, options|
|
||||
options[:project].protected_branch?(repo_branch.name)
|
||||
ProtectedBranch.protected?(options[:project], repo_branch.name)
|
||||
end
|
||||
|
||||
expose :developers_can_push do |repo_branch, options|
|
||||
project = options[:project]
|
||||
access_levels = project.protected_branches.matching(repo_branch.name).map(&:push_access_levels).flatten
|
||||
access_levels.any? { |access_level| access_level.access_level == Gitlab::Access::DEVELOPER }
|
||||
options[:project].protected_branches.developers_can?(:push, repo_branch.name)
|
||||
end
|
||||
|
||||
expose :developers_can_merge do |repo_branch, options|
|
||||
project = options[:project]
|
||||
access_levels = project.protected_branches.matching(repo_branch.name).map(&:merge_access_levels).flatten
|
||||
access_levels.any? { |access_level| access_level.access_level == Gitlab::Access::DEVELOPER }
|
||||
options[:project].protected_branches.developers_can?(:merge, repo_branch.name)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ module Gitlab
|
|||
)
|
||||
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
|
||||
@branch_name = Gitlab::Git.branch_name(@ref)
|
||||
@tag_name = Gitlab::Git.tag_name(@ref)
|
||||
@user_access = user_access
|
||||
@project = project
|
||||
@env = env
|
||||
|
@ -32,11 +33,11 @@ module Gitlab
|
|||
def protected_branch_checks
|
||||
return if skip_authorization
|
||||
return unless @branch_name
|
||||
return unless project.protected_branch?(@branch_name)
|
||||
return unless ProtectedBranch.protected?(project, @branch_name)
|
||||
|
||||
if forced_push?
|
||||
return "You are not allowed to force push code to a protected branch on this project."
|
||||
elsif Gitlab::Git.blank_ref?(@newrev)
|
||||
elsif deletion?
|
||||
return "You are not allowed to delete protected branches from this project."
|
||||
end
|
||||
|
||||
|
@ -58,11 +59,27 @@ module Gitlab
|
|||
def tag_checks
|
||||
return if skip_authorization
|
||||
|
||||
tag_ref = Gitlab::Git.tag_name(@ref)
|
||||
return unless @tag_name
|
||||
|
||||
if tag_ref && protected_tag?(tag_ref) && user_access.cannot_do_action?(:admin_project)
|
||||
"You are not allowed to change existing tags on this project."
|
||||
if tag_exists? && user_access.cannot_do_action?(:admin_project)
|
||||
return "You are not allowed to change existing tags on this project."
|
||||
end
|
||||
|
||||
protected_tag_checks
|
||||
end
|
||||
|
||||
def protected_tag_checks
|
||||
return unless tag_protected?
|
||||
return "Protected tags cannot be updated." if update?
|
||||
return "Protected tags cannot be deleted." if deletion?
|
||||
|
||||
unless user_access.can_create_tag?(@tag_name)
|
||||
return "You are not allowed to create this tag as it is protected."
|
||||
end
|
||||
end
|
||||
|
||||
def tag_protected?
|
||||
ProtectedTag.protected?(project, @tag_name)
|
||||
end
|
||||
|
||||
def push_checks
|
||||
|
@ -75,14 +92,22 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def protected_tag?(tag_name)
|
||||
project.repository.tag_exists?(tag_name)
|
||||
def tag_exists?
|
||||
project.repository.tag_exists?(@tag_name)
|
||||
end
|
||||
|
||||
def forced_push?
|
||||
Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev, env: @env)
|
||||
end
|
||||
|
||||
def update?
|
||||
!Gitlab::Git.blank_ref?(@oldrev) && !deletion?
|
||||
end
|
||||
|
||||
def deletion?
|
||||
Gitlab::Git.blank_ref?(@newrev)
|
||||
end
|
||||
|
||||
def matching_merge_request?
|
||||
Checks::MatchingMergeRequest.new(@newrev, @branch_name, @project).match?
|
||||
end
|
||||
|
|
|
@ -47,6 +47,8 @@ project_tree:
|
|||
- protected_branches:
|
||||
- :merge_access_levels
|
||||
- :push_access_levels
|
||||
- protected_tags:
|
||||
- :create_access_levels
|
||||
- :project_feature
|
||||
|
||||
# Only include the following attributes for the models specified.
|
||||
|
|
|
@ -52,7 +52,11 @@ module Gitlab
|
|||
create_sub_relations(relation, @tree_hash) if relation.is_a?(Hash)
|
||||
|
||||
relation_key = relation.is_a?(Hash) ? relation.keys.first : relation
|
||||
relation_hash = create_relation(relation_key, @tree_hash[relation_key.to_s])
|
||||
relation_hash_list = @tree_hash[relation_key.to_s]
|
||||
|
||||
next unless relation_hash_list
|
||||
|
||||
relation_hash = create_relation(relation_key, relation_hash_list)
|
||||
saved << restored_project.append_or_update_attribute(relation_key, relation_hash)
|
||||
end
|
||||
saved.all?
|
||||
|
|
|
@ -10,6 +10,7 @@ module Gitlab
|
|||
hooks: 'ProjectHook',
|
||||
merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
|
||||
push_access_levels: 'ProtectedBranch::PushAccessLevel',
|
||||
create_access_levels: 'ProtectedTag::CreateAccessLevel',
|
||||
labels: :project_labels,
|
||||
priorities: :label_priorities,
|
||||
label: :project_label }.freeze
|
||||
|
|
|
@ -28,14 +28,23 @@ module Gitlab
|
|||
true
|
||||
end
|
||||
|
||||
def can_create_tag?(ref)
|
||||
return false unless can_access_git?
|
||||
|
||||
if ProtectedTag.protected?(project, ref)
|
||||
project.protected_tags.protected_ref_accessible_to?(ref, user, action: :create)
|
||||
else
|
||||
user.can?(:push_code, project)
|
||||
end
|
||||
end
|
||||
|
||||
def can_push_to_branch?(ref)
|
||||
return false unless can_access_git?
|
||||
|
||||
if project.protected_branch?(ref)
|
||||
if ProtectedBranch.protected?(project, ref)
|
||||
return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user)
|
||||
|
||||
access_levels = project.protected_branches.matching(ref).map(&:push_access_levels).flatten
|
||||
has_access = access_levels.any? { |access_level| access_level.check_access(user) }
|
||||
has_access = project.protected_branches.protected_ref_accessible_to?(ref, user, action: :push)
|
||||
|
||||
has_access || !project.repository.branch_exists?(ref) && can_merge_to_branch?(ref)
|
||||
else
|
||||
|
@ -46,9 +55,8 @@ module Gitlab
|
|||
def can_merge_to_branch?(ref)
|
||||
return false unless can_access_git?
|
||||
|
||||
if project.protected_branch?(ref)
|
||||
access_levels = project.protected_branches.matching(ref).map(&:merge_access_levels).flatten
|
||||
access_levels.any? { |access_level| access_level.check_access(user) }
|
||||
if ProtectedBranch.protected?(project, ref)
|
||||
project.protected_branches.protected_ref_accessible_to?(ref, user, action: :merge)
|
||||
else
|
||||
user.can?(:push_code, project)
|
||||
end
|
||||
|
|
|
@ -3,6 +3,7 @@ require('spec_helper')
|
|||
describe Projects::ProtectedBranchesController do
|
||||
describe "GET #index" do
|
||||
let(:project) { create(:project_empty_repo, :public) }
|
||||
|
||||
it "redirects empty repo to projects page" do
|
||||
get(:index, namespace_id: project.namespace.to_param, project_id: project)
|
||||
end
|
||||
|
|
11
spec/controllers/projects/protected_tags_controller_spec.rb
Normal file
11
spec/controllers/projects/protected_tags_controller_spec.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
require('spec_helper')
|
||||
|
||||
describe Projects::ProtectedTagsController do
|
||||
describe "GET #index" do
|
||||
let(:project) { create(:project_empty_repo, :public) }
|
||||
|
||||
it "redirects empty repo to projects page" do
|
||||
get(:index, namespace_id: project.namespace.to_param, project_id: project)
|
||||
end
|
||||
end
|
||||
end
|
22
spec/factories/protected_tags.rb
Normal file
22
spec/factories/protected_tags.rb
Normal file
|
@ -0,0 +1,22 @@
|
|||
FactoryGirl.define do
|
||||
factory :protected_tag do
|
||||
name
|
||||
project
|
||||
|
||||
after(:build) do |protected_tag|
|
||||
protected_tag.create_access_levels.new(access_level: Gitlab::Access::MASTER)
|
||||
end
|
||||
|
||||
trait :developers_can_create do
|
||||
after(:create) do |protected_tag|
|
||||
protected_tag.create_access_levels.first.update!(access_level: Gitlab::Access::DEVELOPER)
|
||||
end
|
||||
end
|
||||
|
||||
trait :no_one_can_create do
|
||||
after(:create) do |protected_tag|
|
||||
protected_tag.create_access_levels.first.update!(access_level: Gitlab::Access::NO_ACCESS)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,7 +2,9 @@ RSpec.shared_examples "protected branches > access control > CE" do
|
|||
ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
|
||||
it "allows creating protected branches that #{access_type_name} can push to" do
|
||||
visit namespace_project_protected_branches_path(project.namespace, project)
|
||||
|
||||
set_protected_branch_name('master')
|
||||
|
||||
within('.new_protected_branch') do
|
||||
allowed_to_push_button = find(".js-allowed-to-push")
|
||||
|
||||
|
@ -11,6 +13,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
|
|||
within(".dropdown.open .dropdown-menu") { click_on access_type_name }
|
||||
end
|
||||
end
|
||||
|
||||
click_on "Protect"
|
||||
|
||||
expect(ProtectedBranch.count).to eq(1)
|
||||
|
@ -19,7 +22,9 @@ RSpec.shared_examples "protected branches > access control > CE" do
|
|||
|
||||
it "allows updating protected branches so that #{access_type_name} can push to them" do
|
||||
visit namespace_project_protected_branches_path(project.namespace, project)
|
||||
|
||||
set_protected_branch_name('master')
|
||||
|
||||
click_on "Protect"
|
||||
|
||||
expect(ProtectedBranch.count).to eq(1)
|
||||
|
@ -34,6 +39,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
|
|||
end
|
||||
|
||||
wait_for_ajax
|
||||
|
||||
expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id)
|
||||
end
|
||||
end
|
||||
|
@ -41,7 +47,9 @@ RSpec.shared_examples "protected branches > access control > CE" do
|
|||
ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
|
||||
it "allows creating protected branches that #{access_type_name} can merge to" do
|
||||
visit namespace_project_protected_branches_path(project.namespace, project)
|
||||
|
||||
set_protected_branch_name('master')
|
||||
|
||||
within('.new_protected_branch') do
|
||||
allowed_to_merge_button = find(".js-allowed-to-merge")
|
||||
|
||||
|
@ -50,6 +58,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
|
|||
within(".dropdown.open .dropdown-menu") { click_on access_type_name }
|
||||
end
|
||||
end
|
||||
|
||||
click_on "Protect"
|
||||
|
||||
expect(ProtectedBranch.count).to eq(1)
|
||||
|
@ -58,7 +67,9 @@ RSpec.shared_examples "protected branches > access control > CE" do
|
|||
|
||||
it "allows updating protected branches so that #{access_type_name} can merge to them" do
|
||||
visit namespace_project_protected_branches_path(project.namespace, project)
|
||||
|
||||
set_protected_branch_name('master')
|
||||
|
||||
click_on "Protect"
|
||||
|
||||
expect(ProtectedBranch.count).to eq(1)
|
||||
|
@ -73,6 +84,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
|
|||
end
|
||||
|
||||
wait_for_ajax
|
||||
|
||||
expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id)
|
||||
end
|
||||
end
|
||||
|
|
46
spec/features/protected_tags/access_control_ce_spec.rb
Normal file
46
spec/features/protected_tags/access_control_ce_spec.rb
Normal file
|
@ -0,0 +1,46 @@
|
|||
RSpec.shared_examples "protected tags > access control > CE" do
|
||||
ProtectedTag::CreateAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
|
||||
it "allows creating protected tags that #{access_type_name} can create" do
|
||||
visit namespace_project_protected_tags_path(project.namespace, project)
|
||||
|
||||
set_protected_tag_name('master')
|
||||
|
||||
within('.js-new-protected-tag') do
|
||||
allowed_to_create_button = find(".js-allowed-to-create")
|
||||
|
||||
unless allowed_to_create_button.text == access_type_name
|
||||
allowed_to_create_button.click
|
||||
within(".dropdown.open .dropdown-menu") { click_on access_type_name }
|
||||
end
|
||||
end
|
||||
|
||||
click_on "Protect"
|
||||
|
||||
expect(ProtectedTag.count).to eq(1)
|
||||
expect(ProtectedTag.last.create_access_levels.map(&:access_level)).to eq([access_type_id])
|
||||
end
|
||||
|
||||
it "allows updating protected tags so that #{access_type_name} can create them" do
|
||||
visit namespace_project_protected_tags_path(project.namespace, project)
|
||||
|
||||
set_protected_tag_name('master')
|
||||
|
||||
click_on "Protect"
|
||||
|
||||
expect(ProtectedTag.count).to eq(1)
|
||||
|
||||
within(".protected-tags-list") do
|
||||
find(".js-allowed-to-create").click
|
||||
|
||||
within('.js-allowed-to-create-container') do
|
||||
expect(first("li")).to have_content("Roles")
|
||||
click_on access_type_name
|
||||
end
|
||||
end
|
||||
|
||||
wait_for_ajax
|
||||
|
||||
expect(ProtectedTag.last.create_access_levels.map(&:access_level)).to include(access_type_id)
|
||||
end
|
||||
end
|
||||
end
|
94
spec/features/protected_tags_spec.rb
Normal file
94
spec/features/protected_tags_spec.rb
Normal file
|
@ -0,0 +1,94 @@
|
|||
require 'spec_helper'
|
||||
Dir["./spec/features/protected_tags/*.rb"].sort.each { |f| require f }
|
||||
|
||||
feature 'Projected Tags', feature: true, js: true do
|
||||
include WaitForAjax
|
||||
|
||||
let(:user) { create(:user, :admin) }
|
||||
let(:project) { create(:project) }
|
||||
|
||||
before { login_as(user) }
|
||||
|
||||
def set_protected_tag_name(tag_name)
|
||||
find(".js-protected-tag-select").click
|
||||
find(".dropdown-input-field").set(tag_name)
|
||||
click_on("Create wildcard #{tag_name}")
|
||||
end
|
||||
|
||||
describe "explicit protected tags" do
|
||||
it "allows creating explicit protected tags" do
|
||||
visit namespace_project_protected_tags_path(project.namespace, project)
|
||||
set_protected_tag_name('some-tag')
|
||||
click_on "Protect"
|
||||
|
||||
within(".protected-tags-list") { expect(page).to have_content('some-tag') }
|
||||
expect(ProtectedTag.count).to eq(1)
|
||||
expect(ProtectedTag.last.name).to eq('some-tag')
|
||||
end
|
||||
|
||||
it "displays the last commit on the matching tag if it exists" do
|
||||
commit = create(:commit, project: project)
|
||||
project.repository.add_tag(user, 'some-tag', commit.id)
|
||||
|
||||
visit namespace_project_protected_tags_path(project.namespace, project)
|
||||
set_protected_tag_name('some-tag')
|
||||
click_on "Protect"
|
||||
|
||||
within(".protected-tags-list") { expect(page).to have_content(commit.id[0..7]) }
|
||||
end
|
||||
|
||||
it "displays an error message if the named tag does not exist" do
|
||||
visit namespace_project_protected_tags_path(project.namespace, project)
|
||||
set_protected_tag_name('some-tag')
|
||||
click_on "Protect"
|
||||
|
||||
within(".protected-tags-list") { expect(page).to have_content('tag was removed') }
|
||||
end
|
||||
end
|
||||
|
||||
describe "wildcard protected tags" do
|
||||
it "allows creating protected tags with a wildcard" do
|
||||
visit namespace_project_protected_tags_path(project.namespace, project)
|
||||
set_protected_tag_name('*-stable')
|
||||
click_on "Protect"
|
||||
|
||||
within(".protected-tags-list") { expect(page).to have_content('*-stable') }
|
||||
expect(ProtectedTag.count).to eq(1)
|
||||
expect(ProtectedTag.last.name).to eq('*-stable')
|
||||
end
|
||||
|
||||
it "displays the number of matching tags" do
|
||||
project.repository.add_tag(user, 'production-stable', 'master')
|
||||
project.repository.add_tag(user, 'staging-stable', 'master')
|
||||
|
||||
visit namespace_project_protected_tags_path(project.namespace, project)
|
||||
set_protected_tag_name('*-stable')
|
||||
click_on "Protect"
|
||||
|
||||
within(".protected-tags-list") { expect(page).to have_content("2 matching tags") }
|
||||
end
|
||||
|
||||
it "displays all the tags matching the wildcard" do
|
||||
project.repository.add_tag(user, 'production-stable', 'master')
|
||||
project.repository.add_tag(user, 'staging-stable', 'master')
|
||||
project.repository.add_tag(user, 'development', 'master')
|
||||
|
||||
visit namespace_project_protected_tags_path(project.namespace, project)
|
||||
set_protected_tag_name('*-stable')
|
||||
click_on "Protect"
|
||||
|
||||
visit namespace_project_protected_tags_path(project.namespace, project)
|
||||
click_on "2 matching tags"
|
||||
|
||||
within(".protected-tags-list") do
|
||||
expect(page).to have_content("production-stable")
|
||||
expect(page).to have_content("staging-stable")
|
||||
expect(page).not_to have_content("development")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "access control" do
|
||||
include_examples "protected tags > access control > CE"
|
||||
end
|
||||
end
|
|
@ -5,13 +5,10 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
|
|||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:user_access) { Gitlab::UserAccess.new(user, project: project) }
|
||||
let(:changes) do
|
||||
{
|
||||
oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c',
|
||||
newrev: '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51',
|
||||
ref: 'refs/heads/master'
|
||||
}
|
||||
end
|
||||
let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' }
|
||||
let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
|
||||
let(:ref) { 'refs/heads/master' }
|
||||
let(:changes) { { oldrev: oldrev, newrev: newrev, ref: ref } }
|
||||
let(:protocol) { 'ssh' }
|
||||
|
||||
subject do
|
||||
|
@ -23,7 +20,7 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
|
|||
).exec
|
||||
end
|
||||
|
||||
before { allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true) }
|
||||
before { project.add_developer(user) }
|
||||
|
||||
context 'without failed checks' do
|
||||
it "doesn't return any error" do
|
||||
|
@ -41,25 +38,67 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
|
|||
end
|
||||
|
||||
context 'tags check' do
|
||||
let(:changes) do
|
||||
{
|
||||
oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c',
|
||||
newrev: '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51',
|
||||
ref: 'refs/tags/v1.0.0'
|
||||
}
|
||||
end
|
||||
let(:ref) { 'refs/tags/v1.0.0' }
|
||||
|
||||
it 'returns 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)
|
||||
expect(user_access).to receive(:can_do_action?).with(:admin_project).and_return(false)
|
||||
|
||||
expect(subject.status).to be(false)
|
||||
expect(subject.message).to eq('You are not allowed to change existing tags on this project.')
|
||||
end
|
||||
|
||||
context 'with protected tag' do
|
||||
let!(:protected_tag) { create(:protected_tag, project: project, name: 'v*') }
|
||||
|
||||
context 'as master' do
|
||||
before { project.add_master(user) }
|
||||
|
||||
context 'deletion' do
|
||||
let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' }
|
||||
let(:newrev) { '0000000000000000000000000000000000000000' }
|
||||
|
||||
it 'is prevented' do
|
||||
expect(subject.status).to be(false)
|
||||
expect(subject.message).to include('cannot be deleted')
|
||||
end
|
||||
end
|
||||
|
||||
context 'update' do
|
||||
let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' }
|
||||
let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
|
||||
|
||||
it 'is prevented' do
|
||||
expect(subject.status).to be(false)
|
||||
expect(subject.message).to include('cannot be updated')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'creation' do
|
||||
let(:oldrev) { '0000000000000000000000000000000000000000' }
|
||||
let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
|
||||
let(:ref) { 'refs/tags/v9.1.0' }
|
||||
|
||||
it 'prevents creation below access level' do
|
||||
expect(subject.status).to be(false)
|
||||
expect(subject.message).to include('allowed to create this tag as it is protected')
|
||||
end
|
||||
|
||||
context 'when user has access' do
|
||||
let!(:protected_tag) { create(:protected_tag, :developers_can_create, project: project, name: 'v*') }
|
||||
|
||||
it 'allows tag creation' do
|
||||
expect(subject.status).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'protected branches check' do
|
||||
before do
|
||||
allow(project).to receive(:protected_branch?).with('master').and_return(true)
|
||||
allow(ProtectedBranch).to receive(:protected?).with(project, 'master').and_return(true)
|
||||
end
|
||||
|
||||
it 'returns an error if the user is not allowed to do forced pushes to protected branches' do
|
||||
|
@ -86,13 +125,7 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
|
|||
end
|
||||
|
||||
context 'branch deletion' do
|
||||
let(:changes) do
|
||||
{
|
||||
oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c',
|
||||
newrev: '0000000000000000000000000000000000000000',
|
||||
ref: 'refs/heads/master'
|
||||
}
|
||||
end
|
||||
let(:newrev) { '0000000000000000000000000000000000000000' }
|
||||
|
||||
it 'returns an error if the user is not allowed to delete protected branches' do
|
||||
expect(subject.status).to be(false)
|
||||
|
|
|
@ -124,10 +124,15 @@ protected_branches:
|
|||
- project
|
||||
- merge_access_levels
|
||||
- push_access_levels
|
||||
protected_tags:
|
||||
- project
|
||||
- create_access_levels
|
||||
merge_access_levels:
|
||||
- protected_branch
|
||||
push_access_levels:
|
||||
- protected_branch
|
||||
create_access_levels:
|
||||
- protected_tag
|
||||
container_repositories:
|
||||
- project
|
||||
- name
|
||||
|
@ -188,6 +193,7 @@ project:
|
|||
- snippets
|
||||
- hooks
|
||||
- protected_branches
|
||||
- protected_tags
|
||||
- project_members
|
||||
- users
|
||||
- requesters
|
||||
|
|
|
@ -7455,6 +7455,24 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"protected_tags": [
|
||||
{
|
||||
"id": 1,
|
||||
"project_id": 9,
|
||||
"name": "v*",
|
||||
"created_at": "2017-04-04T13:48:13.426Z",
|
||||
"updated_at": "2017-04-04T13:48:13.426Z",
|
||||
"create_access_levels": [
|
||||
{
|
||||
"id": 1,
|
||||
"protected_tag_id": 1,
|
||||
"access_level": 40,
|
||||
"created_at": "2017-04-04T13:48:13.458Z",
|
||||
"updated_at": "2017-04-04T13:48:13.458Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"project_feature": {
|
||||
"builds_access_level": 0,
|
||||
"created_at": "2014-12-26T09:26:45.000Z",
|
||||
|
|
|
@ -64,6 +64,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
|
|||
expect(ProtectedBranch.first.push_access_levels).not_to be_empty
|
||||
end
|
||||
|
||||
it 'contains the create access levels on a protected tag' do
|
||||
expect(ProtectedTag.first.create_access_levels).not_to be_empty
|
||||
end
|
||||
|
||||
context 'event at forth level of the tree' do
|
||||
let(:event) { Event.where(title: 'test levels').first }
|
||||
|
||||
|
|
|
@ -313,6 +313,12 @@ ProtectedBranch:
|
|||
- name
|
||||
- created_at
|
||||
- updated_at
|
||||
ProtectedTag:
|
||||
- id
|
||||
- project_id
|
||||
- name
|
||||
- created_at
|
||||
- updated_at
|
||||
Project:
|
||||
- description
|
||||
- issues_enabled
|
||||
|
@ -346,6 +352,14 @@ ProtectedBranch::PushAccessLevel:
|
|||
- access_level
|
||||
- created_at
|
||||
- updated_at
|
||||
ProtectedTag::CreateAccessLevel:
|
||||
- id
|
||||
- protected_tag_id
|
||||
- access_level
|
||||
- created_at
|
||||
- updated_at
|
||||
- user_id
|
||||
- group_id
|
||||
AwardEmoji:
|
||||
- id
|
||||
- user_id
|
||||
|
|
|
@ -142,4 +142,73 @@ describe Gitlab::UserAccess, lib: true do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'can_create_tag?' do
|
||||
describe 'push to none protected tag' do
|
||||
it 'returns true if user is a master' do
|
||||
project.add_user(user, :master)
|
||||
|
||||
expect(access.can_create_tag?('random_tag')).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns true if user is a developer' do
|
||||
project.add_user(user, :developer)
|
||||
|
||||
expect(access.can_create_tag?('random_tag')).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns false if user is a reporter' do
|
||||
project.add_user(user, :reporter)
|
||||
|
||||
expect(access.can_create_tag?('random_tag')).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe 'push to protected tag' do
|
||||
let(:tag) { create(:protected_tag, project: project, name: "test") }
|
||||
let(:not_existing_tag) { create :protected_tag, project: project }
|
||||
|
||||
it 'returns true if user is a master' do
|
||||
project.add_user(user, :master)
|
||||
|
||||
expect(access.can_create_tag?(tag.name)).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns false if user is a developer' do
|
||||
project.add_user(user, :developer)
|
||||
|
||||
expect(access.can_create_tag?(tag.name)).to be_falsey
|
||||
end
|
||||
|
||||
it 'returns false if user is a reporter' do
|
||||
project.add_user(user, :reporter)
|
||||
|
||||
expect(access.can_create_tag?(tag.name)).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe 'push to protected tag if allowed for developers' do
|
||||
before do
|
||||
@tag = create(:protected_tag, :developers_can_create, project: project)
|
||||
end
|
||||
|
||||
it 'returns true if user is a master' do
|
||||
project.add_user(user, :master)
|
||||
|
||||
expect(access.can_create_tag?(@tag.name)).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns true if user is a developer' do
|
||||
project.add_user(user, :developer)
|
||||
|
||||
expect(access.can_create_tag?(@tag.name)).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns false if user is a reporter' do
|
||||
project.add_user(user, :reporter)
|
||||
|
||||
expect(access.can_create_tag?(@tag.name)).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -441,7 +441,7 @@ describe MergeRequest, models: true do
|
|||
end
|
||||
|
||||
it "can't be removed when its a protected branch" do
|
||||
allow(subject.source_project).to receive(:protected_branch?).and_return(true)
|
||||
allow(ProtectedBranch).to receive(:protected?).and_return(true)
|
||||
expect(subject.can_remove_source_branch?(user)).to be_falsey
|
||||
end
|
||||
|
||||
|
|
|
@ -704,25 +704,6 @@ describe Project, models: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#open_branches' do
|
||||
let(:project) { create(:project, :repository) }
|
||||
|
||||
before do
|
||||
project.protected_branches.create(name: 'master')
|
||||
end
|
||||
|
||||
it { expect(project.open_branches.map(&:name)).to include('feature') }
|
||||
it { expect(project.open_branches.map(&:name)).not_to include('master') }
|
||||
|
||||
it "includes branches matching a protected branch wildcard" do
|
||||
expect(project.open_branches.map(&:name)).to include('feature')
|
||||
|
||||
create(:protected_branch, name: 'feat*', project: project)
|
||||
|
||||
expect(Project.find(project.id).open_branches.map(&:name)).to include('feature')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#star_count' do
|
||||
it 'counts stars from multiple users' do
|
||||
user1 = create :user
|
||||
|
@ -1297,62 +1278,6 @@ describe Project, models: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#protected_branch?' do
|
||||
context 'existing project' do
|
||||
let(:project) { create(:project, :repository) }
|
||||
|
||||
it 'returns true when the branch matches a protected branch via direct match' do
|
||||
create(:protected_branch, project: project, name: "foo")
|
||||
|
||||
expect(project.protected_branch?('foo')).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns true when the branch matches a protected branch via wildcard match' do
|
||||
create(:protected_branch, project: project, name: "production/*")
|
||||
|
||||
expect(project.protected_branch?('production/some-branch')).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns false when the branch does not match a protected branch via direct match' do
|
||||
expect(project.protected_branch?('foo')).to eq(false)
|
||||
end
|
||||
|
||||
it 'returns false when the branch does not match a protected branch via wildcard match' do
|
||||
create(:protected_branch, project: project, name: "production/*")
|
||||
|
||||
expect(project.protected_branch?('staging/some-branch')).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
context "new project" do
|
||||
let(:project) { create(:empty_project) }
|
||||
|
||||
it 'returns false when default_protected_branch is unprotected' do
|
||||
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
|
||||
|
||||
expect(project.protected_branch?('master')).to be false
|
||||
end
|
||||
|
||||
it 'returns false when default_protected_branch lets developers push' do
|
||||
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
|
||||
|
||||
expect(project.protected_branch?('master')).to be false
|
||||
end
|
||||
|
||||
it 'returns true when default_branch_protection does not let developers push but let developer merge branches' do
|
||||
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
|
||||
|
||||
expect(project.protected_branch?('master')).to be true
|
||||
end
|
||||
|
||||
it 'returns true when default_branch_protection is in full protection' do
|
||||
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
|
||||
|
||||
expect(project.protected_branch?('master')).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#user_can_push_to_empty_repo?' do
|
||||
let(:project) { create(:empty_project) }
|
||||
let(:user) { create(:user) }
|
||||
|
|
25
spec/models/protectable_dropdown_spec.rb
Normal file
25
spec/models/protectable_dropdown_spec.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe ProtectableDropdown, models: true do
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:subject) { described_class.new(project, :branches) }
|
||||
|
||||
describe '#protectable_ref_names' do
|
||||
before do
|
||||
project.protected_branches.create(name: 'master')
|
||||
end
|
||||
|
||||
it { expect(subject.protectable_ref_names).to include('feature') }
|
||||
it { expect(subject.protectable_ref_names).not_to include('master') }
|
||||
|
||||
it "includes branches matching a protected branch wildcard" do
|
||||
expect(subject.protectable_ref_names).to include('feature')
|
||||
|
||||
create(:protected_branch, name: 'feat*', project: project)
|
||||
|
||||
subject = described_class.new(project.reload, :branches)
|
||||
|
||||
expect(subject.protectable_ref_names).to include('feature')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -113,8 +113,8 @@ describe ProtectedBranch, models: true do
|
|||
staging = build(:protected_branch, name: "staging")
|
||||
|
||||
expect(ProtectedBranch.matching("production")).to be_empty
|
||||
expect(ProtectedBranch.matching("production", protected_branches: [production, staging])).to include(production)
|
||||
expect(ProtectedBranch.matching("production", protected_branches: [production, staging])).not_to include(staging)
|
||||
expect(ProtectedBranch.matching("production", protected_refs: [production, staging])).to include(production)
|
||||
expect(ProtectedBranch.matching("production", protected_refs: [production, staging])).not_to include(staging)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -132,8 +132,64 @@ describe ProtectedBranch, models: true do
|
|||
staging = build(:protected_branch, name: "staging/*")
|
||||
|
||||
expect(ProtectedBranch.matching("production/some-branch")).to be_empty
|
||||
expect(ProtectedBranch.matching("production/some-branch", protected_branches: [production, staging])).to include(production)
|
||||
expect(ProtectedBranch.matching("production/some-branch", protected_branches: [production, staging])).not_to include(staging)
|
||||
expect(ProtectedBranch.matching("production/some-branch", protected_refs: [production, staging])).to include(production)
|
||||
expect(ProtectedBranch.matching("production/some-branch", protected_refs: [production, staging])).not_to include(staging)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#protected?' do
|
||||
context 'existing project' do
|
||||
let(:project) { create(:project, :repository) }
|
||||
|
||||
it 'returns true when the branch matches a protected branch via direct match' do
|
||||
create(:protected_branch, project: project, name: "foo")
|
||||
|
||||
expect(ProtectedBranch.protected?(project, 'foo')).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns true when the branch matches a protected branch via wildcard match' do
|
||||
create(:protected_branch, project: project, name: "production/*")
|
||||
|
||||
expect(ProtectedBranch.protected?(project, 'production/some-branch')).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns false when the branch does not match a protected branch via direct match' do
|
||||
expect(ProtectedBranch.protected?(project, 'foo')).to eq(false)
|
||||
end
|
||||
|
||||
it 'returns false when the branch does not match a protected branch via wildcard match' do
|
||||
create(:protected_branch, project: project, name: "production/*")
|
||||
|
||||
expect(ProtectedBranch.protected?(project, 'staging/some-branch')).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
context "new project" do
|
||||
let(:project) { create(:empty_project) }
|
||||
|
||||
it 'returns false when default_protected_branch is unprotected' do
|
||||
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
|
||||
|
||||
expect(ProtectedBranch.protected?(project, 'master')).to be false
|
||||
end
|
||||
|
||||
it 'returns false when default_protected_branch lets developers push' do
|
||||
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
|
||||
|
||||
expect(ProtectedBranch.protected?(project, 'master')).to be false
|
||||
end
|
||||
|
||||
it 'returns true when default_branch_protection does not let developers push but let developer merge branches' do
|
||||
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
|
||||
|
||||
expect(ProtectedBranch.protected?(project, 'master')).to be true
|
||||
end
|
||||
|
||||
it 'returns true when default_branch_protection is in full protection' do
|
||||
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
|
||||
|
||||
expect(ProtectedBranch.protected?(project, 'master')).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
12
spec/models/protected_tag_spec.rb
Normal file
12
spec/models/protected_tag_spec.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe ProtectedTag, models: true do
|
||||
describe 'Associations' do
|
||||
it { is_expected.to belong_to(:project) }
|
||||
end
|
||||
|
||||
describe 'Validation' do
|
||||
it { is_expected.to validate_presence_of(:project) }
|
||||
it { is_expected.to validate_presence_of(:name) }
|
||||
end
|
||||
end
|
26
spec/services/protected_branches/update_service_spec.rb
Normal file
26
spec/services/protected_branches/update_service_spec.rb
Normal file
|
@ -0,0 +1,26 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe ProtectedBranches::UpdateService, services: true do
|
||||
let(:protected_branch) { create(:protected_branch) }
|
||||
let(:project) { protected_branch.project }
|
||||
let(:user) { project.owner }
|
||||
let(:params) { { name: 'new protected branch name' } }
|
||||
|
||||
describe '#execute' do
|
||||
subject(:service) { described_class.new(project, user, params) }
|
||||
|
||||
it 'updates a protected branch' do
|
||||
result = service.execute(protected_branch)
|
||||
|
||||
expect(result.reload.name).to eq(params[:name])
|
||||
end
|
||||
|
||||
context 'without admin_project permissions' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it "raises error" do
|
||||
expect{ service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
21
spec/services/protected_tags/create_service_spec.rb
Normal file
21
spec/services/protected_tags/create_service_spec.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe ProtectedTags::CreateService, services: true do
|
||||
let(:project) { create(:empty_project) }
|
||||
let(:user) { project.owner }
|
||||
let(:params) do
|
||||
{
|
||||
name: 'master',
|
||||
create_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }]
|
||||
}
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
subject(:service) { described_class.new(project, user, params) }
|
||||
|
||||
it 'creates a new protected tag' do
|
||||
expect { service.execute }.to change(ProtectedTag, :count).by(1)
|
||||
expect(project.protected_tags.last.create_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
|
||||
end
|
||||
end
|
||||
end
|
26
spec/services/protected_tags/update_service_spec.rb
Normal file
26
spec/services/protected_tags/update_service_spec.rb
Normal file
|
@ -0,0 +1,26 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe ProtectedTags::UpdateService, services: true do
|
||||
let(:protected_tag) { create(:protected_tag) }
|
||||
let(:project) { protected_tag.project }
|
||||
let(:user) { project.owner }
|
||||
let(:params) { { name: 'new protected tag name' } }
|
||||
|
||||
describe '#execute' do
|
||||
subject(:service) { described_class.new(project, user, params) }
|
||||
|
||||
it 'updates a protected tag' do
|
||||
result = service.execute(protected_tag)
|
||||
|
||||
expect(result.reload.name).to eq(params[:name])
|
||||
end
|
||||
|
||||
context 'without admin_project permissions' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it "raises error" do
|
||||
expect{ service.execute(protected_tag) }.to raise_error(Gitlab::Access::AccessDeniedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue