summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames Edwards-Jones <jedwardsjones@gitlab.com>2017-03-15 22:29:07 +0000
committerJames Edwards-Jones <jedwardsjones@gitlab.com>2017-03-31 19:37:01 +0100
commit91ed8ed687ee9edbda0098475e66ad41f886d7a5 (patch)
tree7a446fa1048484d2ccb25a37e7ce650884b7d76f
parent1a416a42f1c1b876ecd96687e41696bc915cc2c2 (diff)
downloadgitlab-ce-91ed8ed687ee9edbda0098475e66ad41f886d7a5.tar.gz
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
-rw-r--r--app/controllers/projects/protected_tags_controller.rb58
-rw-r--r--app/models/concerns/protected_ref_access.rb21
-rw-r--r--app/models/concerns/protected_tag_access.rb21
-rw-r--r--app/models/project.rb1
-rw-r--r--app/models/protected_branch.rb39
-rw-r--r--app/models/protected_ref_matcher.rb52
-rw-r--r--app/models/protected_tag.rb39
-rw-r--r--app/models/protected_tag/push_access_level.rb21
-rw-r--r--app/services/protected_tags/create_service.rb11
-rw-r--r--app/services/protected_tags/update_service.rb13
-rw-r--r--config/routes/project.rb2
-rw-r--r--spec/controllers/projects/protected_tags_controller_spec.rb10
-rw-r--r--spec/factories/protected_tags.rb22
-rw-r--r--spec/features/protected_tags/access_control_ce_spec.rb79
-rw-r--r--spec/features/protected_tags_spec.rb94
-rw-r--r--spec/models/protected_tag_spec.rb14
-rw-r--r--spec/services/protected_tags/create_service_spec.rb23
17 files changed, 488 insertions, 32 deletions
diff --git a/app/controllers/projects/protected_tags_controller.rb b/app/controllers/projects/protected_tags_controller.rb
new file mode 100644
index 00000000000..5ab5d1d997b
--- /dev/null
+++ b/app/controllers/projects/protected_tags_controller.rb
@@ -0,0 +1,58 @@
+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
diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb
new file mode 100644
index 00000000000..9dd4d9c6f24
--- /dev/null
+++ b/app/models/concerns/protected_ref_access.rb
@@ -0,0 +1,21 @@
+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
diff --git a/app/models/concerns/protected_tag_access.rb b/app/models/concerns/protected_tag_access.rb
new file mode 100644
index 00000000000..cf66a6434b5
--- /dev/null
+++ b/app/models/concerns/protected_tag_access.rb
@@ -0,0 +1,21 @@
+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
diff --git a/app/models/project.rb b/app/models/project.rb
index 4a3faff7d5b..3f1a8a1a1e1 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -132,6 +132,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'
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 39e979ef15b..7681d5b5112 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -18,50 +18,25 @@ class ProtectedBranch < ActiveRecord::Base
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
diff --git a/app/models/protected_ref_matcher.rb b/app/models/protected_ref_matcher.rb
new file mode 100644
index 00000000000..83f44240259
--- /dev/null
+++ b/app/models/protected_ref_matcher.rb
@@ -0,0 +1,52 @@
+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
diff --git a/app/models/protected_tag.rb b/app/models/protected_tag.rb
new file mode 100644
index 00000000000..d307549aa49
--- /dev/null
+++ b/app/models/protected_tag.rb
@@ -0,0 +1,39 @@
+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
diff --git a/app/models/protected_tag/push_access_level.rb b/app/models/protected_tag/push_access_level.rb
new file mode 100644
index 00000000000..9282af841ce
--- /dev/null
+++ b/app/models/protected_tag/push_access_level.rb
@@ -0,0 +1,21 @@
+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
diff --git a/app/services/protected_tags/create_service.rb b/app/services/protected_tags/create_service.rb
new file mode 100644
index 00000000000..faba7865a17
--- /dev/null
+++ b/app/services/protected_tags/create_service.rb
@@ -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
diff --git a/app/services/protected_tags/update_service.rb b/app/services/protected_tags/update_service.rb
new file mode 100644
index 00000000000..8a2419efd7b
--- /dev/null
+++ b/app/services/protected_tags/update_service.rb
@@ -0,0 +1,13 @@
+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
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 44b8ae7aedd..f0735bf73e8 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -134,6 +134,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
diff --git a/spec/controllers/projects/protected_tags_controller_spec.rb b/spec/controllers/projects/protected_tags_controller_spec.rb
new file mode 100644
index 00000000000..729017c1483
--- /dev/null
+++ b/spec/controllers/projects/protected_tags_controller_spec.rb
@@ -0,0 +1,10 @@
+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
diff --git a/spec/factories/protected_tags.rb b/spec/factories/protected_tags.rb
new file mode 100644
index 00000000000..f0016b37d66
--- /dev/null
+++ b/spec/factories/protected_tags.rb
@@ -0,0 +1,22 @@
+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
diff --git a/spec/features/protected_tags/access_control_ce_spec.rb b/spec/features/protected_tags/access_control_ce_spec.rb
new file mode 100644
index 00000000000..545d3bca74d
--- /dev/null
+++ b/spec/features/protected_tags/access_control_ce_spec.rb
@@ -0,0 +1,79 @@
+# 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
diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb
new file mode 100644
index 00000000000..546d6037ef7
--- /dev/null
+++ b/spec/features/protected_tags_spec.rb
@@ -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
diff --git a/spec/models/protected_tag_spec.rb b/spec/models/protected_tag_spec.rb
new file mode 100644
index 00000000000..05ad532935a
--- /dev/null
+++ b/spec/models/protected_tag_spec.rb
@@ -0,0 +1,14 @@
+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
diff --git a/spec/services/protected_tags/create_service_spec.rb b/spec/services/protected_tags/create_service_spec.rb
new file mode 100644
index 00000000000..e1fd73dbd07
--- /dev/null
+++ b/spec/services/protected_tags/create_service_spec.rb
@@ -0,0 +1,23 @@
+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