Commit 854ec76c authored by charlie ablett's avatar charlie ablett

Merge branch 'dep-approval-multi-access-levels-rails' into 'master'

Set an approval rule to deployment approval

See merge request gitlab-org/gitlab!83495
parents 5c790805 10ab7777
---
name: deployment_approval_rules
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83495
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354726
milestone: '14.10'
type: development
group: group::release
default_enabled: false
...@@ -89,6 +89,20 @@ module EE ...@@ -89,6 +89,20 @@ module EE
associated_protected_environments.map(&:required_approval_count).max associated_protected_environments.map(&:required_approval_count).max
end end
def has_approval_rules?
return false unless ::Feature.enabled?(:deployment_approval_rules, project, default_enabled: :yaml)
associated_approval_rules.any?
end
def find_approval_rule_for(user, represented_as: nil)
associated_approval_rules.find do |rule|
next if represented_as && rule.humanize.exclude?(represented_as)
rule.check_access(user)
end
end
private private
def protected_environment_accesses(user) def protected_environment_accesses(user)
...@@ -106,5 +120,12 @@ module EE ...@@ -106,5 +120,12 @@ module EE
::ProtectedEnvironment.for_environment(self) ::ProtectedEnvironment.for_environment(self)
end end
end end
def associated_approval_rules
strong_memoize(:associated_approval_rules) do
::ProtectedEnvironments::ApprovalRule
.where(protected_environment: associated_protected_environments)
end
end
end end
end end
...@@ -19,7 +19,7 @@ module EE ...@@ -19,7 +19,7 @@ module EE
project.protected_tags.create_access_by_group(group).delete_all project.protected_tags.create_access_by_group(group).delete_all
# For protected environments # For protected environments
project.protected_environments.deploy_access_levels_by_group(group).delete_all project.protected_environments.revoke_group(group)
end end
end end
end end
...@@ -33,7 +33,7 @@ module EE ...@@ -33,7 +33,7 @@ module EE
def delete_protected_environment_acceses def delete_protected_environment_acceses
return unless user.present? && project.present? return unless user.present? && project.present?
project.protected_environments.deploy_access_levels_by_user(user).delete_all project.protected_environments.revoke_user(user)
end end
def gma_enforcement def gma_enforcement
......
...@@ -25,16 +25,32 @@ class ProtectedEnvironment < ApplicationRecord ...@@ -25,16 +25,32 @@ class ProtectedEnvironment < ApplicationRecord
end end
class << self class << self
def deploy_access_levels_by_user(user) def revoke_user(user)
ProtectedEnvironment::DeployAccessLevel transaction do
.where(protected_environment_id: select(:id)) ProtectedEnvironment::DeployAccessLevel
.where(user: user) .where(protected_environment_id: select(:id))
.where(user: user)
.delete_all
ProtectedEnvironments::ApprovalRule
.where(protected_environment_id: select(:id))
.where(user: user)
.delete_all
end
end end
def deploy_access_levels_by_group(group) def revoke_group(group)
ProtectedEnvironment::DeployAccessLevel transaction do
.where(protected_environment_id: select(:id)) ProtectedEnvironment::DeployAccessLevel
.where(group: group) .where(protected_environment_id: select(:id))
.where(group: group)
.delete_all
ProtectedEnvironments::ApprovalRule
.where(protected_environment_id: select(:id))
.where(group: group)
.delete_all
end
end end
def for_environment(environment) def for_environment(environment)
......
...@@ -2,7 +2,15 @@ ...@@ -2,7 +2,15 @@
module Deployments module Deployments
class ApprovalService < ::BaseService class ApprovalService < ::BaseService
include Gitlab::Utils::StrongMemoize
attr_reader :deployment
delegate :environment, to: :deployment
def execute(deployment, status) def execute(deployment, status)
@deployment = deployment
error_message = validate(deployment, status) error_message = validate(deployment, status)
return error(error_message) if error_message return error(error_message) if error_message
...@@ -22,7 +30,11 @@ module Deployments ...@@ -22,7 +30,11 @@ module Deployments
approval.tap { |a| a.update(status: status, comment: comment) } approval.tap { |a| a.update(status: status, comment: comment) }
else else
deployment.approvals.create(user: current_user, status: status, comment: comment) if environment.has_approval_rules?
deployment.approvals.create(user: current_user, status: status, comment: comment, approval_rule: approval_rule)
else
deployment.approvals.create(user: current_user, status: status, comment: comment)
end
end end
end end
...@@ -42,11 +54,25 @@ module Deployments ...@@ -42,11 +54,25 @@ module Deployments
return _('This environment is not protected.') unless deployment.environment.protected? return _('This environment is not protected.') unless deployment.environment.protected?
return _("You don't have permission to review this deployment. Contact the project or group owner for help.") unless current_user&.can?(:update_deployment, deployment) if environment.has_approval_rules?
unless current_user&.can?(:read_deployment, deployment) && approval_rule
return _("You don't have permission to review this deployment. Contact the project or group owner for help.")
end
else
unless current_user&.can?(:update_deployment, deployment)
return _("You don't have permission to review this deployment. Contact the project or group owner for help.")
end
end
return _('This deployment is not waiting for approvals.') unless deployment.blocked? return _('This deployment is not waiting for approvals.') unless deployment.blocked?
_('You cannot approve your own deployment.') if deployment.user == current_user && status == 'approved' _('You cannot approve your own deployment.') if deployment.user == current_user && status == 'approved'
end end
def approval_rule
strong_memoize(:approval_rule) do
environment.find_approval_rule_for(current_user, represented_as: params[:represented_as])
end
end
end end
end end
...@@ -18,6 +18,7 @@ module EE ...@@ -18,6 +18,7 @@ module EE
requires :deployment_id, type: String, desc: 'The Deployment ID' requires :deployment_id, type: String, desc: 'The Deployment ID'
requires :status, type: String, values: ::Deployments::Approval.statuses.keys requires :status, type: String, values: ::Deployments::Approval.statuses.keys
optional :comment, type: String, desc: 'A comment to go with the approval' optional :comment, type: String, desc: 'A comment to go with the approval'
optional :represented_as, type: String, desc: 'The name of the User/Group/Role to use for the approval, when the user belongs to multiple approval rules.'
end end
post ':id/deployments/:deployment_id/approval' do post ':id/deployments/:deployment_id/approval' do
deployment = user_project.deployments.find(params[:deployment_id]) deployment = user_project.deployments.find(params[:deployment_id])
......
...@@ -53,6 +53,17 @@ RSpec.describe ProjectGroupLink do ...@@ -53,6 +53,17 @@ RSpec.describe ProjectGroupLink do
let(:access_levels) { protected_environment.deploy_access_levels } let(:access_levels) { protected_environment.deploy_access_levels }
it_behaves_like 'deleted related access levels', ProtectedEnvironment::DeployAccessLevel it_behaves_like 'deleted related access levels', ProtectedEnvironment::DeployAccessLevel
context 'with approval rules' do
let(:access_levels) { protected_environment.approval_rules }
before do
create(:protected_environment_approval_rule, protected_environment: protected_environment, group: group)
create(:protected_environment_approval_rule, protected_environment: protected_environment, user: user)
end
it_behaves_like 'deleted related access levels', ::ProtectedEnvironments::ApprovalRule
end
end end
end end
end end
...@@ -5,8 +5,9 @@ require 'spec_helper' ...@@ -5,8 +5,9 @@ require 'spec_helper'
RSpec.describe Environment, :use_clean_rails_memory_store_caching do RSpec.describe Environment, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers include ReactiveCachingHelpers
let(:project) { create(:project, :repository) } let_it_be_with_refind(:group) { create(:group) }
let(:environment) { create(:environment, project: project) } let_it_be_with_refind(:project) { create(:project, :repository, group: group) }
let_it_be_with_refind(:environment) { create(:environment, project: project) }
it { is_expected.to have_many(:dora_daily_metrics) } it { is_expected.to have_many(:dora_daily_metrics) }
...@@ -280,8 +281,6 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do ...@@ -280,8 +281,6 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
describe '#required_approval_count' do describe '#required_approval_count' do
subject { environment.required_approval_count } subject { environment.required_approval_count }
let_it_be(:project) { create(:project, group: create(:group)) }
context 'when Protected Environments feature is not available' do context 'when Protected Environments feature is not available' do
before do before do
stub_licensed_features(protected_environments: false) stub_licensed_features(protected_environments: false)
...@@ -321,4 +320,86 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do ...@@ -321,4 +320,86 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end end
end end
end end
describe '#has_approval_rules?' do
subject { environment.has_approval_rules? }
let_it_be(:protected_environment) { create(:protected_environment, name: environment.name, project: project) }
it { is_expected.to eq(false) }
context 'with approval rules' do
let!(:approval_rule) { create(:protected_environment_approval_rule, :maintainer_access, protected_environment: protected_environment) }
it { is_expected.to eq(true) }
context 'when deployment_approval_rules feature flag is disabled' do
before do
stub_feature_flags(deployment_approval_rules: false)
end
it { is_expected.to eq(false) }
end
end
end
describe '#find_approval_rule_for' do
subject { environment.find_approval_rule_for(user, represented_as: represented_as) }
let_it_be(:qa_group) { create(:group, name: 'QA') }
let_it_be(:security_group) { create(:group, name: 'Security') }
let_it_be(:qa_user) { create(:user) }
let_it_be(:security_user) { create(:user) }
let_it_be(:super_user) { create(:user) }
let_it_be(:protected_environment) { create(:protected_environment, name: environment.name, project: project) }
let(:user) { qa_user }
let(:represented_as) { }
before_all do
qa_group.add_developer(qa_user)
qa_group.add_developer(super_user)
security_group.add_developer(security_user)
security_group.add_developer(super_user)
end
it { is_expected.to be_nil }
context 'with approval rules' do
let!(:approval_rule_for_qa) { create(:protected_environment_approval_rule, group: qa_group, protected_environment: protected_environment) }
let!(:approval_rule_for_security) { create(:protected_environment_approval_rule, group: security_group, protected_environment: protected_environment) }
context 'when user belongs to QA group' do
let(:user) { qa_user }
it { is_expected.to eq(approval_rule_for_qa) }
end
context 'when user belongs to Security group' do
let(:user) { security_user }
it { is_expected.to eq(approval_rule_for_security) }
end
context 'when user belongs to both groups' do
let(:user) { super_user }
it 'returns one of the rules' do
expect([approval_rule_for_qa, approval_rule_for_security]).to include(subject)
end
context 'when represented as QA group' do
let(:represented_as) { 'QA' }
it { is_expected.to eq(approval_rule_for_qa) }
end
context 'when represented as Security group' do
let(:represented_as) { 'Security' }
it { is_expected.to eq(approval_rule_for_security) }
end
end
end
end
end end
...@@ -205,7 +205,7 @@ RSpec.describe ProtectedEnvironment do ...@@ -205,7 +205,7 @@ RSpec.describe ProtectedEnvironment do
end end
end end
describe '.deploy_access_levels_by_user' do describe '.revoke_user' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project) } let(:project) { create(:project) }
let(:environment) { create(:environment, project: project, name: 'production') } let(:environment) { create(:environment, project: project, name: 'production') }
...@@ -217,9 +217,13 @@ RSpec.describe ProtectedEnvironment do ...@@ -217,9 +217,13 @@ RSpec.describe ProtectedEnvironment do
create_deploy_access_level(protected_environment, group: create(:group)) create_deploy_access_level(protected_environment, group: create(:group))
end end
it 'returns matching deploy access levels for the given user' do it 'deletes matching deploy access levels for the given user' do
expect(described_class.deploy_access_levels_by_user(user)) expect(protected_environment.deploy_access_levels).to include(deploy_access_level_for_user)
.to contain_exactly(deploy_access_level_for_user)
described_class.revoke_user(user)
protected_environment.reload
expect(protected_environment.deploy_access_levels).not_to include(deploy_access_level_for_user)
end end
context 'when user is assigned to protected environment in the other project' do context 'when user is assigned to protected environment in the other project' do
...@@ -227,28 +231,38 @@ RSpec.describe ProtectedEnvironment do ...@@ -227,28 +231,38 @@ RSpec.describe ProtectedEnvironment do
let(:other_protected_environment) { create(:protected_environment, project: other_project, name: 'production') } let(:other_protected_environment) { create(:protected_environment, project: other_project, name: 'production') }
let(:other_deploy_access_level_for_user) { create_deploy_access_level(other_protected_environment, user: user) } let(:other_deploy_access_level_for_user) { create_deploy_access_level(other_protected_environment, user: user) }
it 'returns matching deploy access levels for the given user in the specific project' do it 'deletes matching deploy access levels for the given user in the specific project' do
expect(project.protected_environments.deploy_access_levels_by_user(user)) expect(protected_environment.deploy_access_levels).to include(deploy_access_level_for_user)
.to contain_exactly(deploy_access_level_for_user) expect(other_protected_environment.deploy_access_levels).to include(other_deploy_access_level_for_user)
expect(other_project.protected_environments.deploy_access_levels_by_user(user))
.to contain_exactly(other_deploy_access_level_for_user) project.protected_environments.revoke_user(user)
other_project.protected_environments.revoke_user(user)
protected_environment.reload
other_protected_environment.reload
expect(protected_environment.deploy_access_levels).not_to include(deploy_access_level_for_user)
expect(other_protected_environment.deploy_access_levels).not_to include(other_deploy_access_level_for_user)
end end
end end
end end
describe '.deploy_access_levels_by_group' do describe '.revoke_group' do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:project) { create(:project) } let(:project) { create(:project) }
let(:environment) { create(:environment, project: project, name: 'production') } let(:environment) { create(:environment, project: project, name: 'production') }
let(:protected_environment) { create(:protected_environment, project: project, name: 'production') } let(:protected_environment) { create(:protected_environment, project: project, name: 'production') }
let(:deploy_access_level_for_group) { create_deploy_access_level(protected_environment, group: group) } let(:deploy_access_level_for_group) { create_deploy_access_level(protected_environment, group: group) }
it 'returns matching deploy access levels for the given group' do it 'deletes matching deploy access levels for the given group' do
_deploy_access_level_for_different_group = create_deploy_access_level(protected_environment, group: create(:group)) _deploy_access_level_for_different_group = create_deploy_access_level(protected_environment, group: create(:group))
_deploy_access_level_for_user = create_deploy_access_level(protected_environment, user: create(:user)) _deploy_access_level_for_user = create_deploy_access_level(protected_environment, user: create(:user))
expect(described_class.deploy_access_levels_by_group(group)) expect(protected_environment.deploy_access_levels).to include(deploy_access_level_for_group)
.to contain_exactly(deploy_access_level_for_group)
described_class.revoke_group(group)
protected_environment.reload
expect(protected_environment.deploy_access_levels).not_to include(deploy_access_level_for_group)
end end
context 'when user is assigned to protected environment in the other project' do context 'when user is assigned to protected environment in the other project' do
...@@ -257,10 +271,16 @@ RSpec.describe ProtectedEnvironment do ...@@ -257,10 +271,16 @@ RSpec.describe ProtectedEnvironment do
let(:other_deploy_access_level_for_group) { create_deploy_access_level(other_protected_environment, group: group) } let(:other_deploy_access_level_for_group) { create_deploy_access_level(other_protected_environment, group: group) }
it 'returns matching deploy access levels for the given group in the specific project' do it 'returns matching deploy access levels for the given group in the specific project' do
expect(project.protected_environments.deploy_access_levels_by_group(group)) expect(protected_environment.deploy_access_levels).to include(deploy_access_level_for_group)
.to contain_exactly(deploy_access_level_for_group) expect(other_protected_environment.deploy_access_levels).to include(other_deploy_access_level_for_group)
expect(other_project.protected_environments.deploy_access_levels_by_group(group))
.to contain_exactly(other_deploy_access_level_for_group) project.protected_environments.revoke_group(group)
other_project.protected_environments.revoke_group(group)
protected_environment.reload
other_protected_environment.reload
expect(protected_environment.deploy_access_levels).not_to include(deploy_access_level_for_group)
expect(other_protected_environment.deploy_access_levels).not_to include(other_deploy_access_level_for_group)
end end
end end
end end
......
...@@ -14,10 +14,10 @@ RSpec.describe Deployments::ApprovalService do ...@@ -14,10 +14,10 @@ RSpec.describe Deployments::ApprovalService do
let(:required_approval_count) { 2 } let(:required_approval_count) { 2 }
let(:build) { create(:ci_build, :manual, project: project) } let(:build) { create(:ci_build, :manual, project: project) }
let(:deployment) { create(:deployment, :blocked, project: project, environment: environment, deployable: build) } let(:deployment) { create(:deployment, :blocked, project: project, environment: environment, deployable: build) }
let!(:protected_environment) { create(:protected_environment, :maintainers_can_deploy, name: environment.name, project: project, required_approval_count: required_approval_count) }
before do before do
stub_licensed_features(protected_environments: true) stub_licensed_features(protected_environments: true)
create(:protected_environment, :maintainers_can_deploy, name: environment.name, project: project, required_approval_count: required_approval_count)
project.add_maintainer(user) if user project.add_maintainer(user) if user
end end
...@@ -61,12 +61,37 @@ RSpec.describe Deployments::ApprovalService do ...@@ -61,12 +61,37 @@ RSpec.describe Deployments::ApprovalService do
end end
end end
shared_examples_for 'set approval rule' do
context 'with approval rule' do
let!(:approval_rule) { create(:protected_environment_approval_rule, :maintainer_access, protected_environment: protected_environment) }
it 'sets an rule to the deployment approval' do
expect(subject[:status]).to eq(:success)
expect(subject[:approval].approval_rule).to eq(approval_rule)
expect(::Deployments::Approval.last.approval_rule).to eq(approval_rule)
end
context 'when deployment_approval_rules feature flag is disabled' do
before do
stub_feature_flags(deployment_approval_rules: false)
end
it 'does not set an rule to the deployment approval' do
expect(subject[:status]).to eq(:success)
expect(subject[:approval].approval_rule).to be_nil
expect(::Deployments::Approval.last.approval_rule).to be_nil
end
end
end
end
describe '#execute' do describe '#execute' do
subject { service.execute(deployment, status) } subject { service.execute(deployment, status) }
context 'when status is approved' do context 'when status is approved' do
include_examples 'approve' include_examples 'approve'
include_examples 'comment' include_examples 'comment'
include_examples 'set approval rule'
end end
context 'when status is rejected' do context 'when status is rejected' do
...@@ -74,6 +99,7 @@ RSpec.describe Deployments::ApprovalService do ...@@ -74,6 +99,7 @@ RSpec.describe Deployments::ApprovalService do
include_examples 'reject' include_examples 'reject'
include_examples 'comment' include_examples 'comment'
include_examples 'set approval rule'
end end
context 'when user already approved' do context 'when user already approved' do
...@@ -182,6 +208,26 @@ RSpec.describe Deployments::ApprovalService do ...@@ -182,6 +208,26 @@ RSpec.describe Deployments::ApprovalService do
include_examples 'error', message: "You don't have permission to review this deployment. Contact the project or group owner for help." include_examples 'error', message: "You don't have permission to review this deployment. Contact the project or group owner for help."
end end
context 'with approval rule' do
let!(:approval_rule) { create(:protected_environment_approval_rule, :maintainer_access, protected_environment: protected_environment) }
context 'when the user does not have permission to read deployment' do
before do
project.add_guest(user)
end
include_examples 'error', message: "You don't have permission to review this deployment. Contact the project or group owner for help."
end
context 'when there are no rules for the user' do
before do
project.add_developer(user)
end
include_examples 'error', message: "You don't have permission to review this deployment. Contact the project or group owner for help."
end
end
context 'when user is nil' do context 'when user is nil' do
let(:user) { nil } let(:user) { nil }
......
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