Read about our upcoming Code of Conduct on this issue

Commit 91ed8ed6 authored by James Edwards-Jones's avatar James Edwards-Jones
Browse files

Protected tags copy/paste from protected branches

Should provide basic CRUD backend for frontend to work from. Doesn’t include frontend, API, or the internal API used from gitlab-shell
parent 53513c8e706b
class Projects::ProtectedTagsController < Projects::ApplicationController
include RepositorySettingsRedirect
# Authorize
before_action :require_non_empty_project
before_action :authorize_admin_project!
before_action :load_protected_tag, only: [:show, :update, :destroy]
layout "project_settings"
def index
redirect_to_repository_settings(@project)
end
def create
@protected_tag = ::ProtectedTags::CreateService.new(@project, current_user, protected_tag_params).execute
unless @protected_tag.persisted?
flash[:alert] = @protected_tags.errors.full_messages.join(', ').html_safe
end
redirect_to_repository_settings(@project)
end
def show
@matching_tags = @protected_tag.matching(@project.repository.tags)
end
def update
@protected_tag = ::ProtectedTags::UpdateService.new(@project, current_user, protected_tag_params).execute(@protected_tag)
if @protected_tag.valid?
respond_to do |format|
format.json { render json: @protected_tag, status: :ok }
end
else
respond_to do |format|
format.json { render json: @protected_tag.errors, status: :unprocessable_entity }
end
end
end
def destroy
@protected_tag.destroy
respond_to do |format|
format.html { redirect_to_repository_settings(@project) }
format.js { head :ok }
end
end
private
def load_protected_tag
@protected_tag = @project.protected_tags.find(params[:id])
end
def protected_tag_params
params.require(:protected_tag).permit(:name, push_access_levels_attributes: [:access_level, :id])
end
end
module ProtectedBranchAccess
extend ActiveSupport::Concern
included do
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
module ProtectedTagAccess
extend ActiveSupport::Concern
included do
belongs_to :protected_tag
delegate :project, to: :protected_tag
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
......@@ -132,6 +132,7 @@ def update_forks_visibility_level
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'
......
......@@ -18,50 +18,25 @@ def commit
project.commit(self.name)
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) }
ProtectedRefMatcher.matching(ProtectedBranch, branch_name, protected_refs: protected_branches)
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) }
ref_matcher.matching(branches)
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)
ref_matcher.matches?(branch_name)
end
# Checks if this protected branch contains a wildcard
def wildcard?
self.name && self.name.include?('*')
ref_matcher.wildcard?
end
protected
def exact_match?(branch_name)
self.name == branch_name
end
def wildcard_match?(branch_name)
wildcard_regex === branch_name
end
private
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 ref_matcher
@ref_matcher ||= ProtectedRefMatcher.new(self)
end
end
class ProtectedRefMatcher
def initialize(protected_ref)
@protected_ref = protected_ref
end
# Returns all protected refs that match the given ref 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_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)
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
class ProtectedTag < ActiveRecord::Base
include Gitlab::ShellAdapter
belongs_to :project
validates :name, presence: true
validates :project, presence: true
has_many :push_access_levels, dependent: :destroy
validates :push_access_levels, length: { is: 1, message: "are restricted to a single instance per protected tag." }
accepts_nested_attributes_for :push_access_levels
def commit
project.commit(self.name)
end
def self.matching(tag_name, protected_tags: nil)
ProtectedRefMatcher.matching(ProtectedTag, tag_name, protected_refs: protected_tags)
end
def matching(branches)
ref_matcher.matching(branches)
end
def matches?(tag_name)
ref_matcher.matches?(tag_name)
end
def wildcard?
ref_matcher.wildcard?
end
private
def ref_matcher
@ref_matcher ||= ProtectedRefMatcher.new(self)
end
end
class ProtectedTag::PushAccessLevel < 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
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
module ProtectedTags
class UpdateService < BaseService
attr_reader :protected_tag
def execute(protected_tag)
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
@protected_tag = protected_tag
@protected_tag.update(params)
@protected_tag
end
end
end
......@@ -134,6 +134,8 @@
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
......
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
FactoryGirl.define do
factory :protected_tag do
name
project
after(:build) do |protected_tag|
protected_tag.push_access_levels.new(access_level: Gitlab::Access::MASTER)
end
trait :developers_can_push do
after(:create) do |protected_tag|
protected_tag.push_access_levels.first.update!(access_level: Gitlab::Access::DEVELOPER)
end
end
trait :no_one_can_push do
after(:create) do |protected_tag|
protected_tag.push_access_levels.first.update!(access_level: Gitlab::Access::NO_ACCESS)
end
end
end
end
# RSpec.shared_examples "protected tags > access control > CE" do
# ProtectedTag::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
# it "allows creating protected tags that #{access_type_name} can push to" do
# visit namespace_project_protected_tags_path(project.namespace, project)
# set_protected_tag_name('master')
# within('.new_protected_tag') do
# allowed_to_push_button = find(".js-allowed-to-push")
# unless allowed_to_push_button.text == access_type_name
# allowed_to_push_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.push_access_levels.map(&:access_level)).to eq([access_type_id])
# end
# it "allows updating protected tags so that #{access_type_name} can push to 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-push").click
# within('.js-allowed-to-push-container') do
# expect(first("li")).to have_content("Roles")
# click_on access_type_name
# end
# end
# wait_for_ajax
# expect(ProtectedTag.last.push_access_levels.map(&:access_level)).to include(access_type_id)
# end
# end
# ProtectedTag::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
# it "allows creating protected tags that #{access_type_name} can merge to" do
# visit namespace_project_protected_tags_path(project.namespace, project)
# set_protected_tag_name('master')
# within('.new_protected_tag') do
# allowed_to_merge_button = find(".js-allowed-to-merge")
# unless allowed_to_merge_button.text == access_type_name
# allowed_to_merge_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.merge_access_levels.map(&:access_level)).to eq([access_type_id])
# end
# it "allows updating protected tags so that #{access_type_name} can merge to 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-merge").click
# within('.js-allowed-to-merge-container') do
# expect(first("li")).to have_content("Roles")
# click_on access_type_name
# end
# end
# wait_for_ajax
# expect(ProtectedTag.last.merge_access_levels.map(&:access_level)).to include(access_type_id)
# end
# end
# end
# 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
require 'spec_helper'
describe ProtectedTag, models: true do
subject { build_stubbed(:protected_branch) }
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
require 'spec_helper'
describe ProtectedTags::CreateService, services: true do
let(:project) { create(:empty_project) }
let(:user) { project.owner }
let(:params) do
{
name: 'master',
merge_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }],
push_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.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
expect(project.protected_tags.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
end
end
end
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment