diff options
author | James Edwards-Jones <jedwardsjones@gitlab.com> | 2017-03-15 22:29:07 +0000 |
---|---|---|
committer | James Edwards-Jones <jedwardsjones@gitlab.com> | 2017-03-15 22:29:07 +0000 |
commit | a0e8cab31027ed3f8f66413939bbd5f54cce1701 (patch) | |
tree | b8906a0432990f0885ff46dc6fc987b2b98f69e2 | |
parent | ab7c3e91fb7234388abe2e2035fc2dfb43e4a306 (diff) | |
download | gitlab-ce-a0e8cab31027ed3f8f66413939bbd5f54cce1701.tar.gz |
WIP: 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.rb | 58 | ||||
-rw-r--r-- | app/models/concerns/protected_ref_access.rb | 21 | ||||
-rw-r--r-- | app/models/concerns/protected_tag_access.rb | 21 | ||||
-rw-r--r-- | app/models/project.rb | 1 | ||||
-rw-r--r-- | app/models/protected_branch.rb | 39 | ||||
-rw-r--r-- | app/models/protected_ref_matcher.rb | 52 | ||||
-rw-r--r-- | app/models/protected_tag.rb | 39 | ||||
-rw-r--r-- | app/models/protected_tag/push_access_level.rb | 21 | ||||
-rw-r--r-- | app/services/protected_tags/create_service.rb | 11 | ||||
-rw-r--r-- | app/services/protected_tags/update_service.rb | 13 | ||||
-rw-r--r-- | config/routes/project.rb | 2 | ||||
-rw-r--r-- | spec/controllers/projects/protected_tags_controller_spec.rb | 10 | ||||
-rw-r--r-- | spec/factories/protected_tags.rb | 22 | ||||
-rw-r--r-- | spec/features/protected_tags/access_control_ce_spec.rb | 79 | ||||
-rw-r--r-- | spec/features/protected_tags_spec.rb | 94 | ||||
-rw-r--r-- | spec/models/protected_tag_spec.rb | 14 | ||||
-rw-r--r-- | spec/services/protected_tags/create_service_spec.rb | 23 |
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 8c2dadf4659..bb3eb12f3ea 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -133,6 +133,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 |