Commit 80c49f4f authored by Shinya Maeda's avatar Shinya Maeda

Group-level Protected Environment Alpha Version

This commit introduces a database migration
for Group-Level Protected Environment,
which is still alpha version to be tested.

Changelog: other
parent 0470e3ad
...@@ -36,6 +36,10 @@ class GroupMember < Member ...@@ -36,6 +36,10 @@ class GroupMember < Member
Gitlab::Access.sym_options_with_owner Gitlab::Access.sym_options_with_owner
end end
def self.pluck_user_ids
pluck(:user_id)
end
def group def group
source source
end end
......
...@@ -902,6 +902,10 @@ class Project < ApplicationRecord ...@@ -902,6 +902,10 @@ class Project < ApplicationRecord
alias_method :ancestors, :ancestors_upto alias_method :ancestors, :ancestors_upto
def ancestors_upto_ids(...)
ancestors_upto(...).pluck(:id)
end
def emails_disabled? def emails_disabled?
strong_memoize(:emails_disabled) do strong_memoize(:emails_disabled) do
# disabling in the namespace overrides the project setting # disabling in the namespace overrides the project setting
......
---
name: group_level_protected_environments
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61575
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331085
milestone: '14.0'
type: development
group: group::release
default_enabled: false
# frozen_string_literal: true
class GroupProtectedEnvironmentsAddColumn < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
def up
add_column :protected_environments, :group_id, :bigint
change_column_null :protected_environments, :project_id, true
end
def down
change_column_null :protected_environments, :project_id, false
remove_column :protected_environments, :group_id
end
end
# frozen_string_literal: true
class GroupProtectedEnvironmentsAddIndexAndConstraint < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
INDEX_NAME = 'index_protected_environments_on_group_id_and_name'
disable_ddl_transaction!
def up
add_concurrent_index :protected_environments, [:group_id, :name], unique: true,
name: INDEX_NAME, where: 'group_id IS NOT NULL'
add_concurrent_foreign_key :protected_environments, :namespaces, column: :group_id, on_delete: :cascade
add_check_constraint :protected_environments,
"((project_id IS NULL) != (group_id IS NULL))",
:protected_environments_project_or_group_existence
end
def down
remove_group_protected_environments!
remove_check_constraint :protected_environments, :protected_environments_project_or_group_existence
remove_foreign_key_if_exists :protected_environments, column: :group_id
remove_concurrent_index_by_name :protected_environments, name: INDEX_NAME
end
private
def remove_group_protected_environments!
execute <<-SQL
DELETE FROM protected_environments WHERE group_id IS NOT NULL
SQL
end
end
88d2c1507503de626dfdb3f2f0eaf0f51fad5fc2279fd147d901c5dcc7ae91eb
\ No newline at end of file
2c5c0756757a181cf8bf7968de5184664004a82c093ae3fc14c5d6931a1ab44f
\ No newline at end of file
...@@ -17187,10 +17187,12 @@ ALTER SEQUENCE protected_environment_deploy_access_levels_id_seq OWNED BY protec ...@@ -17187,10 +17187,12 @@ ALTER SEQUENCE protected_environment_deploy_access_levels_id_seq OWNED BY protec
CREATE TABLE protected_environments ( CREATE TABLE protected_environments (
id integer NOT NULL, id integer NOT NULL,
project_id integer NOT NULL, project_id integer,
created_at timestamp with time zone NOT NULL, created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL,
name character varying NOT NULL name character varying NOT NULL,
group_id bigint,
CONSTRAINT protected_environments_project_or_group_existence CHECK (((project_id IS NULL) <> (group_id IS NULL)))
); );
CREATE SEQUENCE protected_environments_id_seq CREATE SEQUENCE protected_environments_id_seq
...@@ -24315,6 +24317,8 @@ CREATE INDEX index_protected_environment_deploy_access_levels_on_group_id ON pro ...@@ -24315,6 +24317,8 @@ CREATE INDEX index_protected_environment_deploy_access_levels_on_group_id ON pro
CREATE INDEX index_protected_environment_deploy_access_levels_on_user_id ON protected_environment_deploy_access_levels USING btree (user_id); CREATE INDEX index_protected_environment_deploy_access_levels_on_user_id ON protected_environment_deploy_access_levels USING btree (user_id);
CREATE UNIQUE INDEX index_protected_environments_on_group_id_and_name ON protected_environments USING btree (group_id, name) WHERE (group_id IS NOT NULL);
CREATE INDEX index_protected_environments_on_project_id ON protected_environments USING btree (project_id); CREATE INDEX index_protected_environments_on_project_id ON protected_environments USING btree (project_id);
CREATE UNIQUE INDEX index_protected_environments_on_project_id_and_name ON protected_environments USING btree (project_id, name); CREATE UNIQUE INDEX index_protected_environments_on_project_id_and_name ON protected_environments USING btree (project_id, name);
...@@ -25748,6 +25752,9 @@ ALTER TABLE ONLY issues ...@@ -25748,6 +25752,9 @@ ALTER TABLE ONLY issues
ALTER TABLE ONLY epics ALTER TABLE ONLY epics
ADD CONSTRAINT fk_9d480c64b2 FOREIGN KEY (start_date_sourcing_epic_id) REFERENCES epics(id) ON DELETE SET NULL; ADD CONSTRAINT fk_9d480c64b2 FOREIGN KEY (start_date_sourcing_epic_id) REFERENCES epics(id) ON DELETE SET NULL;
ALTER TABLE ONLY protected_environments
ADD CONSTRAINT fk_9e112565b7 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY alert_management_alerts ALTER TABLE ONLY alert_management_alerts
ADD CONSTRAINT fk_9e49e5c2b7 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; ADD CONSTRAINT fk_9e49e5c2b7 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
...@@ -24,6 +24,7 @@ module EE ...@@ -24,6 +24,7 @@ module EE
has_one :saml_provider has_one :saml_provider
has_many :scim_identities has_many :scim_identities
has_many :ip_restrictions, autosave: true has_many :ip_restrictions, autosave: true
has_many :protected_environments, inverse_of: :group
has_one :insight, foreign_key: :namespace_id has_one :insight, foreign_key: :namespace_id
accepts_nested_attributes_for :insight, allow_destroy: true accepts_nested_attributes_for :insight, allow_destroy: true
has_one :scim_oauth_access_token has_one :scim_oauth_access_token
......
# frozen_string_literal: true # frozen_string_literal: true
class ProtectedEnvironment < ApplicationRecord class ProtectedEnvironment < ApplicationRecord
include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Utils::StrongMemoize
include FromUnion
belongs_to :project belongs_to :project
belongs_to :group, inverse_of: :protected_environments
has_many :deploy_access_levels, inverse_of: :protected_environment has_many :deploy_access_levels, inverse_of: :protected_environment
accepts_nested_attributes_for :deploy_access_levels, allow_destroy: true accepts_nested_attributes_for :deploy_access_levels, allow_destroy: true
validates :deploy_access_levels, length: { minimum: 1 } validates :deploy_access_levels, length: { minimum: 1 }
validates :name, :project, presence: true validates :name, presence: true
validate :valid_tier_name, if: :group_level?
scope :sorted_by_name, -> { order(:name) } scope :sorted_by_name, -> { order(:name) }
...@@ -31,7 +34,10 @@ class ProtectedEnvironment < ApplicationRecord ...@@ -31,7 +34,10 @@ class ProtectedEnvironment < ApplicationRecord
key = "protected_environment:for_environment:#{environment.id}" key = "protected_environment:for_environment:#{environment.id}"
::Gitlab::SafeRequestStore.fetch(key) do ::Gitlab::SafeRequestStore.fetch(key) do
where(project: environment.project_id, name: environment.name) from_union([
where(project: environment.project_id, name: environment.name),
where(group: environment.project.ancestors_upto_ids, name: environment.tier)
])
end end
end end
end end
...@@ -40,4 +46,28 @@ class ProtectedEnvironment < ApplicationRecord ...@@ -40,4 +46,28 @@ class ProtectedEnvironment < ApplicationRecord
deploy_access_levels deploy_access_levels
.any? { |deploy_access_level| deploy_access_level.check_access(user) } .any? { |deploy_access_level| deploy_access_level.check_access(user) }
end end
def container_access_level(user)
if project_level?
project.team.max_member_access(user&.id)
elsif group_level?
group.max_member_access_for_user(user)
end
end
private
def valid_tier_name
unless Environment.tiers[name]
errors.add(:name, "must be one of environment tiers: #{Environment.tiers.keys.join(', ')}.")
end
end
def project_level?
project_id.present?
end
def group_level?
group_id.present?
end
end end
...@@ -14,19 +14,17 @@ class ProtectedEnvironment::DeployAccessLevel < ApplicationRecord ...@@ -14,19 +14,17 @@ class ProtectedEnvironment::DeployAccessLevel < ApplicationRecord
belongs_to :user belongs_to :user
belongs_to :group belongs_to :group
belongs_to :protected_environment belongs_to :protected_environment, inverse_of: :deploy_access_levels
validates :access_level, presence: true, inclusion: { in: ALLOWED_ACCESS_LEVELS } validates :access_level, presence: true, inclusion: { in: ALLOWED_ACCESS_LEVELS }
delegate :project, to: :protected_environment
def check_access(user) def check_access(user)
return false unless user return false unless user
return true if user.admin? return true if user.admin?
return user.id == user_id if user_type? return user.id == user_id if user_type?
return group.users.exists?(user.id) if group_type? return group.users.exists?(user.id) if group_type?
project.team.max_member_access(user.id) >= access_level protected_environment.container_access_level(user) >= access_level
end end
def user_type? def user_type?
......
...@@ -147,6 +147,7 @@ module EE ...@@ -147,6 +147,7 @@ module EE
rule { maintainer }.policy do rule { maintainer }.policy do
enable :maintainer_access enable :maintainer_access
enable :admin_wiki enable :admin_wiki
enable :admin_protected_environment
end end
rule { owner | admin }.policy do rule { owner | admin }.policy do
......
...@@ -36,10 +36,10 @@ module ProtectedEnvironments ...@@ -36,10 +36,10 @@ module ProtectedEnvironments
def qualified_group_ids def qualified_group_ids
strong_memoize(:qualified_group_ids) do strong_memoize(:qualified_group_ids) do
if project_container? if project_container?
container.invited_groups.pluck_primary_key.to_set container.invited_groups
elsif group_container? elsif group_container?
raise NotImplementedError container.self_and_descendants
end end.pluck_primary_key.to_set
end end
end end
...@@ -53,11 +53,9 @@ module ProtectedEnvironments ...@@ -53,11 +53,9 @@ module ProtectedEnvironments
if project_container? if project_container?
container.project_authorizations container.project_authorizations
.visible_to_user_and_access_level(user_ids, Gitlab::Access::DEVELOPER) .visible_to_user_and_access_level(user_ids, Gitlab::Access::DEVELOPER)
.pluck_user_ids
.to_set
elsif group_container? elsif group_container?
raise NotImplementedError container.members_with_parents.owners_and_maintainers
end end.pluck_user_ids.to_set
end end
end end
end end
......
...@@ -6,8 +6,6 @@ module API ...@@ -6,8 +6,6 @@ module API
ENVIRONMENT_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(name: API::NO_SLASH_URL_PART_REGEX) ENVIRONMENT_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(name: API::NO_SLASH_URL_PART_REGEX)
before { authorize_admin_project }
feature_category :continuous_delivery feature_category :continuous_delivery
params do params do
...@@ -15,6 +13,14 @@ module API ...@@ -15,6 +13,14 @@ module API
end end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
helpers do
def protected_environment
@protected_environment ||= user_project.protected_environments.find_by_name!(params[:name])
end
end
before { authorize_admin_project }
desc "Get a project's protected environments" do desc "Get a project's protected environments" do
detail 'This feature was introduced in GitLab 12.8.' detail 'This feature was introduced in GitLab 12.8.'
success ::EE::API::Entities::ProtectedEnvironment success ::EE::API::Entities::ProtectedEnvironment
...@@ -87,9 +93,89 @@ module API ...@@ -87,9 +93,89 @@ module API
end end
end end
params do
requires :id, type: String, desc: 'The ID of the group'
end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
helpers do helpers do
def protected_environment def protected_environment
@protected_environment ||= user_project.protected_environments.find_by_name!(params[:name]) @protected_environment ||= user_group.protected_environments.find_by_name!(params[:name])
end
end
before do
authorize! :admin_protected_environment, user_group
unless Feature.enabled?(:group_level_protected_environments, user_group, default_enabled: :yaml)
not_found!
end
end
desc "Get a group's protected environments" do
detail 'This feature was introduced in GitLab 14.0.'
success ::EE::API::Entities::ProtectedEnvironment
end
params do
use :pagination
end
get ':id/protected_environments' do
protected_environments = user_group.protected_environments.sorted_by_name
present paginate(protected_environments), with: ::EE::API::Entities::ProtectedEnvironment
end
desc 'Get a single protected environment' do
detail 'This feature was introduced in GitLab 14.0.'
success ::EE::API::Entities::ProtectedEnvironment
end
params do
requires :name, type: String, desc: 'The tier name of the protected environment'
end
get ':id/protected_environments/:name' do
present protected_environment, with: ::EE::API::Entities::ProtectedEnvironment
end
desc 'Protect a single environment' do
detail 'This feature was introduced in GitLab 14.0.'
success ::EE::API::Entities::ProtectedEnvironment
end
params do
requires :name, type: String, desc: 'The tier name of the protected environment'
requires :deploy_access_levels, as: :deploy_access_levels_attributes, type: Array, desc: 'An array of users/groups allowed to deploy environment' do
optional :access_level, type: Integer, values: ::ProtectedEnvironment::DeployAccessLevel::ALLOWED_ACCESS_LEVELS
optional :user_id, type: Integer
optional :group_id, type: Integer
end
end
post ':id/protected_environments' do
protected_environment = user_group.protected_environments.find_by_name(params[:name])
if protected_environment
conflict!("Protected environment '#{params[:name]}' already exists")
end
declared_params = declared_params(include_missing: false)
protected_environment = ::ProtectedEnvironments::CreateService
.new(container: user_group, current_user: current_user, params: declared_params).execute
if protected_environment.persisted?
present protected_environment, with: ::EE::API::Entities::ProtectedEnvironment
else
render_api_error!(protected_environment.errors.full_messages, 422)
end
end
desc 'Unprotect a single environment' do
detail 'This feature was introduced in GitLab 14.0.'
end
params do
requires :name, type: String, desc: 'The tier name of the protected environment'
end
delete ':id/protected_environments/:name' do
destroy_conditionally!(protected_environment) do
::ProtectedEnvironments::DestroyService.new(container: user_group, current_user: current_user).execute(protected_environment)
end
end end
end end
end end
......
...@@ -35,8 +35,22 @@ FactoryBot.define do ...@@ -35,8 +35,22 @@ FactoryBot.define do
end end
end end
trait :production do
name { 'production' }
end
trait :staging do trait :staging do
name { 'staging' } name { 'staging' }
end end
trait :project_level do
project
group { nil }
end
trait :group_level do
project { nil }
group
end
end end
end end
...@@ -8,9 +8,39 @@ RSpec.describe ProtectedEnvironment do ...@@ -8,9 +8,39 @@ RSpec.describe ProtectedEnvironment do
end end
describe 'validation' do describe 'validation' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_length_of(:deploy_access_levels) } it { is_expected.to validate_length_of(:deploy_access_levels) }
it 'can not belong to both group and project' do
group = build(:group)
project = build(:project)
protected_environment = build(:protected_environment, group: group, project: project)
expect { protected_environment.save! }.to raise_error(ActiveRecord::StatementInvalid, /PG::CheckViolation/)
end
it 'must belong to one of group or project' do
protected_environment = build(:protected_environment, group: nil, project: nil)
expect { protected_environment.save! }.to raise_error(ActiveRecord::StatementInvalid, /PG::CheckViolation/)
end
context 'group-level protected environment' do
let_it_be(:group) { create(:group) }
it 'passes the validation when the name is listed in the tiers' do
protection = build(:protected_environment, name: 'production', group: group)
expect(protection).to be_valid
end
it 'fails the validation when the name is not listed in the tiers' do
protection = build(:protected_environment, name: 'customer-portal', group: group)
expect(protection).not_to be_valid
expect(protection.errors[:name].first).to include('must be one of environment tiers')
end
end
end end
describe '#accessible_to?' do describe '#accessible_to?' do
...@@ -90,6 +120,58 @@ RSpec.describe ProtectedEnvironment do ...@@ -90,6 +120,58 @@ RSpec.describe ProtectedEnvironment do
end end
end end
describe '#container_access_level' do
subject { protected_environment.container_access_level(user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:maintainer) { create(:user) }
let_it_be(:developer) { create(:user) }
before_all do
project.add_maintainer(maintainer)
project.add_developer(developer)
group.add_maintainer(maintainer)
group.add_developer(developer)
end
shared_examples_for 'correct access levels' do
context 'for project maintainer' do
let(:user) { maintainer }
it { is_expected.to eq(Gitlab::Access::MAINTAINER) }
end
context 'for project developer' do
let(:user) { developer }
it { is_expected.to eq(Gitlab::Access::DEVELOPER) }
end
context 'when user is nil' do
let(:user) { }
it { is_expected.to eq(Gitlab::Access::NO_ACCESS) }
end
end
context 'with project-level protected environment' do
let!(:protected_environment) do
create(:protected_environment, :project_level, project: project)
end
it_behaves_like 'correct access levels'
end
context 'with group-level protected environment' do
let!(:protected_environment) do
create(:protected_environment, :group_level, group: group)
end
it_behaves_like 'correct access levels'
end
end
describe '.sorted_by_name' do describe '.sorted_by_name' do
subject(:protected_environments) { described_class.sorted_by_name } subject(:protected_environments) { described_class.sorted_by_name }
...@@ -132,9 +214,11 @@ RSpec.describe ProtectedEnvironment do ...@@ -132,9 +214,11 @@ RSpec.describe ProtectedEnvironment do
end end
describe '.for_environment' do describe '.for_environment' do
let_it_be(:project, reload: true) { create(:project) } let_it_be(:group) { create(:group) }
let_it_be(:environment) { build(:environment, name: 'production', project: project) } let_it_be(:project, reload: true) { create(:project, group: group) }
let_it_be(:protected_environment) { create(:protected_environment, name: 'production', project: project) }
let!(:environment) { create(:environment, name: 'production', project: project) }
let!(:protected_environment) { create(:protected_environment, name: 'production', project: project) }
subject { described_class.for_environment(environment) } subject { described_class.for_environment(environment) }
...@@ -148,24 +232,52 @@ RSpec.describe ProtectedEnvironment do ...@@ -148,24 +232,52 @@ RSpec.describe ProtectedEnvironment do
end end
context 'when environment is a different name' do context 'when environment is a different name' do
let_it_be(:environment) { build(:environment, name: 'staging', project: project) } let!(:environment) { create(:environment, name: 'staging', project: project) }
it { is_expected.to be_empty } it { is_expected.to be_empty }
end end
context 'when environment exists in a different project' do context 'when environment exists in a different project' do
let_it_be(:environment) { build(:environment, name: 'production', project: create(:project)) } let!(:environment) { create(:environment, name: 'production', project: create(:project)) }
it { is_expected.to be_empty } it { is_expected.to be_empty }
end end
context 'when environment does not exist' do context 'when environment does not exist' do
let(:environment) { } let!(:environment) { }
it 'raises an error' do it 'raises an error' do
expect { subject }.to raise_error(ArgumentError) expect { subject }.to raise_error(ArgumentError)
end end
end end
context 'with group-level protected environment' do
let!(:group_protected_environment) { create(:protected_environment, :production, :group_level, group: group) }
context 'with project-level production environment' do
let!(:environment) { create(:environment, :production, project: project) }
it 'has multiple protections' do
is_expected.to contain_exactly(protected_environment, group_protected_environment)
end
context 'when project-level protection does not exist' do
let!(:protected_environment) { }
it 'has only group-level protection' do
is_expected.to eq([group_protected_environment])
end
end
end
context 'with staging environment' do
let(:environment) { create(:environment, :staging, project: project) }
it 'does not have any protections' do
is_expected.to be_empty
end
end
end
end end
def create_deploy_access_level(**opts) def create_deploy_access_level(**opts)
......
...@@ -4,7 +4,9 @@ require 'spec_helper' ...@@ -4,7 +4,9 @@ require 'spec_helper'
RSpec.describe Ci::BuildPolicy do RSpec.describe Ci::BuildPolicy do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let(:project) { create(:project, :repository) } let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0') } let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0') }
......
...@@ -4,7 +4,9 @@ require 'spec_helper' ...@@ -4,7 +4,9 @@ require 'spec_helper'
RSpec.describe EnvironmentPolicy do RSpec.describe EnvironmentPolicy do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let(:project) { create(:project, :repository) } let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:environment) { create(:environment, :with_review_app, ref: 'development', project: project) } let(:environment) { create(:environment, :with_review_app, ref: 'development', project: project) }
......
...@@ -5,12 +5,15 @@ require 'spec_helper' ...@@ -5,12 +5,15 @@ require 'spec_helper'
RSpec.describe API::ProtectedEnvironments do RSpec.describe API::ProtectedEnvironments do
include AccessMatchersForRequest include AccessMatchersForRequest
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:protected_environment_name) { 'production' } let(:protected_environment_name) { 'production' }
before do before do
create(:protected_environment, :maintainers_can_deploy, project: project, name: protected_environment_name) create(:protected_environment, :maintainers_can_deploy, :project_level, project: project, name: protected_environment_name)
create(:protected_environment, :maintainers_can_deploy, :group_level, group: group, name: protected_environment_name)
end end
shared_examples 'requests for non-maintainers' do shared_examples 'requests for non-maintainers' do
...@@ -162,4 +165,147 @@ RSpec.describe API::ProtectedEnvironments do ...@@ -162,4 +165,147 @@ RSpec.describe API::ProtectedEnvironments do
it_behaves_like 'requests for non-maintainers' it_behaves_like 'requests for non-maintainers'
end end
describe "GET /groups/:id/protected_environments" do
let(:route) { "/groups/#{group.id}/protected_environments" }
let(:request) { get api(route, user), params: { per_page: 100 } }
context 'when authenticated as a maintainer' do
before do
group.add_maintainer(user)
end
it 'returns the protected environments' do
request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
protected_environment_names = json_response.map { |x| x['name'] }
expect(protected_environment_names).to match_array([protected_environment_name])
end
end
it_behaves_like 'requests for non-maintainers'
end
describe "GET /groups/:id/protected_environments/:environment" do
let(:requested_environment_name) { protected_environment_name }
let(:route) { "/groups/#{group.id}/protected_environments/#{requested_environment_name}" }
let(:request) { get api(route, user) }
context 'when authenticated as a maintainer' do
before do
group.add_maintainer(user)
end
it 'returns the protected environment' do
request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/protected_environment', dir: 'ee')
expect(json_response['name']).to eq(protected_environment_name)
expect(json_response['deploy_access_levels'][0]['access_level']).to eq(::Gitlab::Access::MAINTAINER)
end
context 'when protected environment does not exist' do
let(:requested_environment_name) { 'unknown' }
it_behaves_like '404 response' do
let(:message) { '404 Not found' }
end
end
end
it_behaves_like 'requests for non-maintainers'
end
describe 'POST /groups/:id/protected_environments/' do
let(:api_url) { api("/groups/#{group.id}/protected_environments/", user) }
context 'when authenticated as a maintainer' do
before do
group.add_maintainer(user)
end
it 'protects the environment with user allowed to deploy' do
deployer = create(:user)
group.add_maintainer(deployer)
post api_url, params: { name: 'staging', deploy_access_levels: [{ user_id: deployer.id }] }
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('public_api/v4/protected_environment', dir: 'ee')
expect(json_response['name']).to eq('staging')
expect(json_response['deploy_access_levels'].first['user_id']).to eq(deployer.id)
end
it 'protects the environment with group allowed to deploy' do
subgroup = create(:group, parent: group)
post api_url, params: { name: 'staging', deploy_access_levels: [{ group_id: subgroup.id }] }
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('public_api/v4/protected_environment', dir: 'ee')
expect(json_response['name']).to eq('staging')
expect(json_response['deploy_access_levels'].first['group_id']).to eq(subgroup.id)
end
it 'protects the environment with maintainers allowed to deploy' do
post api_url, params: { name: 'staging', deploy_access_levels: [{ access_level: Gitlab::Access::MAINTAINER }] }
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('public_api/v4/protected_environment', dir: 'ee')
expect(json_response['name']).to eq('staging')
expect(json_response['deploy_access_levels'].first['access_level']).to eq(Gitlab::Access::MAINTAINER)
end
it 'returns 409 error if environment is already protected' do
deployer = create(:user)
group.add_developer(deployer)
post api_url, params: { name: 'production', deploy_access_levels: [{ user_id: deployer.id }] }
expect(response).to have_gitlab_http_status(:conflict)
end
context 'without deploy_access_levels' do
it_behaves_like '400 response' do
let(:request) { post api_url, params: { name: 'staging' } }
end
end
it 'returns error with invalid deploy access level' do
post api_url, params: { name: 'staging', deploy_access_levels: [{ access_level: nil }] }
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
end
it_behaves_like 'requests for non-maintainers' do
let(:request) { post api_url, params: { name: 'staging' } }
end
end
describe 'DELETE /groups/:id/protected_environments/:environment' do
let(:route) { "/groups/#{group.id}/protected_environments/production" }
let(:request) { delete api(route, user) }
context 'when authenticated as a maintainer' do
before do
group.add_maintainer(user)
end
it 'unprotects the environment' do
expect do
request
end.to change { group.protected_environments.count }.by(-1)
expect(response).to have_gitlab_http_status(:no_content)
end
end
it_behaves_like 'requests for non-maintainers'
end
end end
...@@ -4,11 +4,13 @@ require 'spec_helper' ...@@ -4,11 +4,13 @@ require 'spec_helper'
RSpec.describe JobEntity do RSpec.describe JobEntity do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:request) { double('request') } let(:request) { double('request') }
let(:entity) { described_class.new(job, request: request) } let(:entity) { described_class.new(job, request: request) }
let(:environment) { create(:environment, project: project) } let(:environment) { create(:environment, :production, project: project) }
before do before do
allow(request).to receive(:current_user).and_return(user) allow(request).to receive(:current_user).and_return(user)
......
...@@ -51,9 +51,10 @@ RSpec.describe EnvironmentEntity do ...@@ -51,9 +51,10 @@ RSpec.describe EnvironmentEntity do
end end
context 'when environment has a review app' do context 'when environment has a review app' do
let(:project) { create(:project, :repository) } let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) }
let(:environment) { create(:environment, :with_review_app, ref: 'development', project: project) } let(:environment) { create(:environment, :with_review_app, ref: 'development', project: project) }
let(:protected_environment) { create(:protected_environment, name: environment.name, project: project) }
before do before do
project.repository.add_branch(user, 'development', project.commit.id) project.repository.add_branch(user, 'development', project.commit.id)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ProtectedEnvironments::BaseService, '#execute' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:other_group) { create(:group) }
let_it_be(:child_group) { create(:group, parent: group) }
let_it_be(:current_user) { create(:user) }
let(:service) do
described_class.new(container: container, current_user: current_user, params: params)
end
describe '#sanitized_params' do
subject { service.send(:sanitized_params) }
context 'with group container' do
let(:container) { group }
context 'with group-based access control' do
let(:params) do
{
deploy_access_levels_attributes: [
{ group_id: group.id },
{ group_id: other_group.id },
{ group_id: child_group.id }
]
}
end
it 'filters out inappropriate group id' do
is_expected.to eq(
deploy_access_levels_attributes: [
{ group_id: group.id },
{ group_id: child_group.id }
]
)
end
end
context 'with user-based access control' do
let(:params) do
{
deploy_access_levels_attributes: [
{ user_id: group_maintainer.id },
{ user_id: group_developer.id },
{ user_id: other_group_maintainer.id },
{ user_id: child_group_maintainer.id }
]
}
end
let!(:group_maintainer) { create(:user) }
let!(:group_developer) { create(:user) }
let!(:other_group_maintainer) { create(:user) }
let!(:child_group_maintainer) { create(:user) }
before do
group.add_maintainer(group_maintainer)
group.add_developer(group_developer)
other_group.add_maintainer(other_group_maintainer)
child_group.add_maintainer(child_group_maintainer)
end
it 'filters out inappropriate user ids' do
is_expected.to eq(
deploy_access_levels_attributes: [
{ user_id: group_maintainer.id }
]
)
end
end
end
end
end
...@@ -35,9 +35,7 @@ RSpec.shared_examples 'protected environments access' do |developer_access: true ...@@ -35,9 +35,7 @@ RSpec.shared_examples 'protected environments access' do |developer_access: true
context 'when Protected Environments feature is available in the project' do context 'when Protected Environments feature is available in the project' do
let(:feature_available) { true } let(:feature_available) { true }
context 'when environment is protected' do shared_examples_for 'authorize correctly per access type' do
let(:protected_environment) { create(:protected_environment, name: environment.name, project: project) }
context 'when user does not have access to the environment' do context 'when user does not have access to the environment' do
where(:access_level, :result) do where(:access_level, :result) do
:guest | false :guest | false
...@@ -77,19 +75,31 @@ RSpec.shared_examples 'protected environments access' do |developer_access: true ...@@ -77,19 +75,31 @@ RSpec.shared_examples 'protected environments access' do |developer_access: true
end end
context 'when the user has access via a group' do context 'when the user has access via a group' do
let(:group) { create(:group) } let(:operator_group) { create(:group) }
before do before do
project.add_reporter(user) project.add_reporter(user)
group.add_reporter(user) operator_group.add_reporter(user)
protected_environment.deploy_access_levels.create!(group: group, access_level: Gitlab::Access::REPORTER) protected_environment.deploy_access_levels.create!(group: operator_group, access_level: Gitlab::Access::REPORTER)
end end
it { is_expected.to eq(direct_access) } it { is_expected.to eq(direct_access) }
end end
end end
context 'when environment is protected with project-level protection' do
let(:protected_environment) { create(:protected_environment, :project_level, name: environment.name, project: project) }
it_behaves_like 'authorize correctly per access type'
end
context 'when environment is protected with group-level protection' do
let(:protected_environment) { create(:protected_environment, :group_level, name: environment.tier, group: group) }
it_behaves_like 'authorize correctly per access type'
end
context 'when environment is not protected' do context 'when environment is not protected' do
where(:access_level, :result) do where(:access_level, :result) do
:guest | false :guest | false
......
...@@ -16,19 +16,19 @@ FactoryBot.define do ...@@ -16,19 +16,19 @@ FactoryBot.define do
end end
trait :production do trait :production do
tier { :production } name { 'production' }
end end
trait :staging do trait :staging do
tier { :staging } name { 'staging' }
end end
trait :testing do trait :testing do
tier { :testing } name { 'testing' }
end end
trait :development do trait :development do
tier { :development } name { 'development' }
end end
trait :with_review_app do |environment| trait :with_review_app do |environment|
......
...@@ -128,7 +128,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Environment do ...@@ -128,7 +128,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Environment do
context 'when environment has already been created' do context 'when environment has already been created' do
before do before do
create(:environment, :staging, project: project, name: 'customer-portal') create(:environment, project: project, name: 'customer-portal', tier: :staging)
end end
it 'does not overwrite the specified deployment tier' do it 'does not overwrite the specified deployment tier' do
......
...@@ -624,6 +624,7 @@ metrics_setting: ...@@ -624,6 +624,7 @@ metrics_setting:
- project - project
protected_environments: protected_environments:
- project - project
- group
- deploy_access_levels - deploy_access_levels
deploy_access_levels: deploy_access_levels:
- protected_environment - protected_environment
......
...@@ -701,6 +701,7 @@ ProjectSetting: ...@@ -701,6 +701,7 @@ ProjectSetting:
ProtectedEnvironment: ProtectedEnvironment:
- id - id
- project_id - project_id
- group_id
- name - name
- created_at - created_at
- updated_at - updated_at
......
# frozen_string_literal: true
require 'spec_helper'
require_migration!('group_protected_environments_add_index_and_constraint')
RSpec.describe GroupProtectedEnvironmentsAddIndexAndConstraint do
let(:migration) { described_class.new }
let(:protected_environments) { table(:protected_environments) }
let(:group) { table(:namespaces).create!(name: 'group', path: 'group') }
let(:project) { table(:projects).create!(name: 'project', path: 'project', namespace_id: group.id) }
describe '#down' do
it 'deletes only group-level configurations' do
migration.up
project_protections = [
protected_environments.create!(project_id: project.id, name: 'production'),
protected_environments.create!(project_id: project.id, name: 'staging')
]
protected_environments.create!(group_id: group.id, name: 'production')
protected_environments.create!(group_id: group.id, name: 'staging')
migration.down
expect(protected_environments.pluck(:id))
.to match_array project_protections.map(&:id)
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