Merge branch '18471-restrict-tag-pushes-protected-tags' into 'master'

Protected Tags

Closes #18471

See merge request !10356
This commit is contained in:
Douwe Maan 2017-04-07 15:43:28 +00:00
commit 46aadc5c16
83 changed files with 1466 additions and 297 deletions

View file

@ -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();

View file

@ -0,0 +1,2 @@
export { default as ProtectedTagCreate } from './protected_tag_create';
export { default as ProtectedTagEditList } from './protected_tag_edit_list';

View file

@ -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();
},
});
}
}

View file

@ -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));
}
}

View file

@ -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);
}
}

View 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();
});
}
}

View file

@ -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),
});
});
}
}

View file

@ -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 {

View file

@ -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])

View 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

View 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

View file

@ -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

View file

@ -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

View file

@ -21,4 +21,8 @@ module TagsHelper
html.html_safe
end
def protected_tag?(project, tag)
ProtectedTag.protected?(project, tag.name)
end
end

View file

@ -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

View 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

View 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

View 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

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View 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

View 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

View 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

View file

@ -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

View file

@ -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,

View file

@ -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

View 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

View 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

View file

@ -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<

View file

@ -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

View file

@ -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

View 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

View 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"

View file

@ -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)

View 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'

View 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'

View file

@ -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 }})

View 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.

View file

@ -3,3 +3,4 @@
= render @deploy_keys
= render "projects/protected_branches/index"
= render "projects/protected_tags/index"

View file

@ -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?
&nbsp;
= 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")

View file

@ -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?

View file

@ -0,0 +1,4 @@
---
title: Tags can be protected, restricting creation of matching tags by user role
merge_request: 10356
author:

View file

@ -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

View file

@ -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',

View 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

View file

@ -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

View file

@ -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.

View file

@ -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 | | | | ✓ | ✓ |

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View 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"

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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?

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View 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

View file

@ -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

View 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

View 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

View file

@ -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)

View file

@ -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

View file

@ -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",

View file

@ -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 }

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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) }

View 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

View file

@ -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

View 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

View 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

View 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

View 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