Commit 5e4ce2f3 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch '2747-protected-environments-backend-ee' into 'master'

Protected Environments - Backend

See merge request gitlab-org/gitlab-ee!6672
parents a63e886d 1dd0d56f
......@@ -506,6 +506,12 @@ export default {
>
{{ model.name }}
</a>
<span
v-if="isProtected"
class="badge badge-success"
>
{{ s__("Environments|protected") }}
</span>
</span>
<span
v-else
......
......@@ -7,6 +7,8 @@ class CommitStatus < ActiveRecord::Base
include Presentable
include EnumWithNil
prepend ::EE::CommitStatus
self.table_name = 'ci_builds'
belongs_to :user
......@@ -50,7 +52,7 @@ class CommitStatus < ActiveRecord::Base
runner_system_failure: 4,
missing_dependency_failure: 5,
runner_unsupported: 6
}
}.merge(EE_FAILURE_REASONS)
##
# We still create some CommitStatuses outside of CreatePipelineService.
......
......@@ -2,6 +2,8 @@
module Ci
class BuildPolicy < CommitStatusPolicy
prepend EE::Ci::BuildPolicy
condition(:protected_ref) do
access = ::Gitlab::UserAccess.new(@user, project: @subject.project)
......
# frozen_string_literal: true
class EnvironmentPolicy < BasePolicy
prepend EE::EnvironmentPolicy
delegate { @subject.project }
condition(:stop_with_deployment_allowed) do
......
......@@ -252,6 +252,7 @@ class ProjectPolicy < BasePolicy
enable :update_pages
enable :read_cluster
enable :create_cluster
enable :create_environment_terminal
end
rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror
......
# frozen_string_literal: true
class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
prepend ::EE::CommitStatusPresenter
CALLOUT_FAILURE_MESSAGES = {
unknown_failure: 'There is an unknown failure, please try again',
script_failure: nil,
......@@ -9,7 +10,7 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
runner_system_failure: 'There has been a runner system failure, please try again',
missing_dependency_failure: 'There has been a missing dependency failure',
runner_unsupported: 'Your runner is outdated, please upgrade your runner'
}.freeze
}.merge(EE_CALLOUT_FAILURE_MESSAGES).freeze
presents :build
......
......@@ -2,7 +2,6 @@
class EnvironmentEntity < Grape::Entity
include RequestAwareEntity
prepend ::EE::EnvironmentEntity
expose :id
......@@ -26,8 +25,7 @@ class EnvironmentEntity < Grape::Entity
stop_project_environment_path(environment.project, environment)
end
expose :terminal_path, if: ->(*) { environment.has_terminals? } do |environment|
can?(request.current_user, :admin_environment, environment.project) &&
expose :terminal_path, if: ->(*) { environment.has_terminals? && can_access_terminal? } do |environment|
terminal_project_environment_path(environment.project, environment)
end
......@@ -52,4 +50,8 @@ class EnvironmentEntity < Grape::Entity
def can_read_deploy_board?
can?(current_user, :read_deploy_board, environment.project)
end
def can_access_terminal?
can?(request.current_user, :create_environment_terminal, environment)
end
end
# frozen_string_literal: true
module Ci
class EnqueueBuildService < BaseService
prepend EE::Ci::EnqueueBuildService
def execute(build)
build.enqueue
end
end
end
......@@ -37,7 +37,7 @@ module Ci
def process_build(build, current_status)
if valid_statuses_for_when(build.when).include?(current_status)
build.action? ? build.actionize : build.enqueue
build.action? ? build.actionize : enqueue_build(build)
true
else
build.skip
......@@ -93,5 +93,9 @@ module Ci
.where.not(id: latest_statuses.map(&:first))
.update_all(retried: true) if latest_statuses.any?
end
def enqueue_build(build)
Ci::EnqueueBuildService.new(project, @user).execute(build)
end
end
end
......@@ -285,6 +285,14 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
## EE-specific
resources :protected_environments, only: [:create, :update, :destroy], constraints: { id: /\d+/ } do
collection do
get 'search'
end
end
## EE-specific
resource :cycle_analytics, only: [:show]
namespace :cycle_analytics do
......
......@@ -920,6 +920,7 @@ ActiveRecord::Schema.define(version: 20180807153545) do
t.string "slug", null: false
end
add_index "environments", ["name"], name: "index_environments_on_name_varchar_pattern_ops", using: :btree, opclasses: {"name"=>"varchar_pattern_ops"}
add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", unique: true, using: :btree
add_index "environments", ["project_id", "slug"], name: "index_environments_on_project_id_and_slug", unique: true, using: :btree
......@@ -2283,6 +2284,29 @@ ActiveRecord::Schema.define(version: 20180807153545) do
add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree
create_table "protected_environment_deploy_access_levels", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.integer "access_level", default: 40
t.integer "protected_environment_id", null: false
t.integer "user_id"
t.integer "group_id"
end
add_index "protected_environment_deploy_access_levels", ["group_id"], name: "index_protected_environment_deploy_access_levels_on_group_id", using: :btree
add_index "protected_environment_deploy_access_levels", ["protected_environment_id"], name: "index_protected_environment_deploy_access", using: :btree
add_index "protected_environment_deploy_access_levels", ["user_id"], name: "index_protected_environment_deploy_access_levels_on_user_id", using: :btree
create_table "protected_environments", force: :cascade do |t|
t.integer "project_id", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.string "name", null: false
end
add_index "protected_environments", ["project_id", "name"], name: "index_protected_environments_on_project_id_and_name", unique: true, using: :btree
add_index "protected_environments", ["project_id"], name: "index_protected_environments_on_project_id", using: :btree
create_table "protected_tag_create_access_levels", force: :cascade do |t|
t.integer "protected_tag_id", null: false
t.integer "access_level", default: 40
......@@ -3089,6 +3113,10 @@ ActiveRecord::Schema.define(version: 20180807153545) do
add_foreign_key "protected_branch_unprotect_access_levels", "protected_branches", on_delete: :cascade
add_foreign_key "protected_branch_unprotect_access_levels", "users", on_delete: :cascade
add_foreign_key "protected_branches", "projects", name: "fk_7a9c6d93e7", on_delete: :cascade
add_foreign_key "protected_environment_deploy_access_levels", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "protected_environment_deploy_access_levels", "protected_environments", on_delete: :cascade
add_foreign_key "protected_environment_deploy_access_levels", "users", on_delete: :cascade
add_foreign_key "protected_environments", "projects", on_delete: :cascade
add_foreign_key "protected_tag_create_access_levels", "namespaces", column: "group_id", name: "fk_b4eb82fe3c", on_delete: :cascade
add_foreign_key "protected_tag_create_access_levels", "protected_tags", name: "fk_f7dfda8c51", on_delete: :cascade
add_foreign_key "protected_tag_create_access_levels", "users"
......
import Vue from 'vue';
import Dashboard from 'ee/vue_shared/license_management/license_management.vue';
import ProtectedEnvironmentCreate from 'ee/protected_environments/protected_environment_create';
import ProtectedEnvironmentEditList from 'ee/protected_environments/protected_environment_edit_list';
import '~/pages/projects/settings/ci_cd/show/index';
document.addEventListener('DOMContentLoaded', () => {
......@@ -18,4 +20,10 @@ document.addEventListener('DOMContentLoaded', () => {
},
});
}
// eslint-disable-next-line no-new
new ProtectedEnvironmentCreate();
// eslint-disable-next-line no-new
new ProtectedEnvironmentEditList();
});
......@@ -49,6 +49,7 @@ export default class AccessDropdown {
e.preventDefault();
if ($el.is('.is-active')) {
if (this.noOneObj) {
if (item.id === this.noOneObj.id) {
// remove all others selected items
this.accessLevelsData.forEach(level => {
......@@ -68,6 +69,7 @@ export default class AccessDropdown {
this.removeSelectedItem(this.noOneObj);
}
}
}
// make element active right away
$el.addClass(`is-active item-${item.type}`);
......
export const ACCESS_LEVELS = {
DEPLOY: 'deploy_access_levels',
};
export const LEVEL_TYPES = {
ROLE: 'role',
USER: 'user',
GROUP: 'group',
};
export const LEVEL_ID_PROP = {
ROLE: 'access_level',
USER: 'user_id',
GROUP: 'group_id',
};
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import AccessorUtilities from '~/lib/utils/accessor';
import Flash from '~/flash';
import CreateItemDropdown from '~/create_item_dropdown';
import AccessDropdown from 'ee/projects/settings/access_dropdown';
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
const PROTECTED_ENVIRONMENT_INPUT = 'input[name="protected_environment[name]"]';
export default class ProtectedEnvironmentCreate {
constructor() {
this.$form = $('.js-new-protected-environment');
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.currentProjectUserDefaults = {};
this.buildDropdowns();
this.bindEvents();
}
bindEvents() {
this.$form.on('submit', this.onFormSubmit.bind(this));
}
buildDropdowns() {
const $allowedToDeployDropdown = this.$form.find('.js-allowed-to-deploy');
// Cache callback
this.onSelectCallback = this.onSelect.bind(this);
// Allowed to Deploy dropdown
this[`${ACCESS_LEVELS.DEPLOY}_dropdown`] = new AccessDropdown({
$dropdown: $allowedToDeployDropdown,
accessLevelsData: gon.deploy_access_levels,
onSelect: this.onSelectCallback,
accessLevel: ACCESS_LEVELS.DEPLOY,
});
this.createItemDropdown = new CreateItemDropdown({
$dropdown: this.$form.find('.js-protected-environment-select'),
defaultToggleLabel: 'Protected Environment',
fieldName: 'protected_environment[name]',
onSelect: this.onSelectCallback,
getData: ProtectedEnvironmentCreate.getProtectedEnvironments,
filterRemote: true,
});
}
// Enable submit button after selecting an option
onSelect() {
const $allowedToDeploy = this[`${ACCESS_LEVELS.DEPLOY}_dropdown`].getSelectedItems();
const toggle = !(
this.$form.find(PROTECTED_ENVIRONMENT_INPUT).val() &&
$allowedToDeploy.length
);
this.$form.find('input[type="submit"]').attr('disabled', toggle);
}
static getProtectedEnvironments(term, callback) {
axios
.get(gon.search_unprotected_environments_url, { params: { query: term } })
.then(({ data }) => {
const environments = [].concat(data);
const results = environments.map(environment => ({
id: environment,
text: environment,
title: environment,
}));
callback(results);
})
.catch(() => {
Flash('An error occured while fetching environments.');
callback([]);
});
}
getFormData() {
const formData = {
authenticity_token: this.$form.find('input[name="authenticity_token"]').val(),
protected_environment: {
name: this.$form.find(PROTECTED_ENVIRONMENT_INPUT).val(),
},
};
Object.keys(ACCESS_LEVELS).forEach(level => {
const accessLevel = ACCESS_LEVELS[level];
const selectedItems = this[`${accessLevel}_dropdown`].getSelectedItems();
const levelAttributes = [];
selectedItems.forEach(item => {
if (item.type === LEVEL_TYPES.USER) {
levelAttributes.push({
user_id: item.user_id,
});
} else if (item.type === LEVEL_TYPES.ROLE) {
levelAttributes.push({
access_level: item.access_level,
});
} else if (item.type === LEVEL_TYPES.GROUP) {
levelAttributes.push({
group_id: item.group_id,
});
}
});
formData.protected_environment[`${accessLevel}_attributes`] = levelAttributes;
});
return formData;
}
onFormSubmit(e) {
e.preventDefault();
axios[this.$form.attr('method')](this.$form.attr('action'), this.getFormData())
.then(() => {
window.location.reload();
})
.catch(() => Flash('Failed to protect the environment'));
}
}
import $ from 'jquery';
import _ from 'underscore';
import axios from '~/lib/utils/axios_utils';
import Flash from '~/flash';
import AccessDropdown from 'ee/projects/settings/access_dropdown';
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
export default class ProtectedEnvironmentEdit {
constructor(options) {
this.$wraps = {};
this.hasChanges = false;
this.$wrap = options.$wrap;
this.$allowedToDeployDropdown = this.$wrap.find('.js-allowed-to-deploy');
this.$wraps[ACCESS_LEVELS.DEPLOY] = this.$allowedToDeployDropdown.closest(
`.${ACCESS_LEVELS.DEPLOY}-container`,
);
this.buildDropdowns();
}
buildDropdowns() {
// Allowed to deploy dropdown
this[`${ACCESS_LEVELS.DEPLOY}_dropdown`] = new AccessDropdown({
accessLevel: ACCESS_LEVELS.deploy,
accessLevelsData: gon.deploy_access_levels,
$dropdown: this.$allowedToDeployDropdown,
onSelect: this.onSelectOption.bind(this),
onHide: this.onDropdownHide.bind(this),
});
}
onSelectOption() {
this.hasChanges = true;
}
onDropdownHide() {
if (!this.hasChanges) {
return;
}
this.hasChanges = true;
this.updatePermissions();
}
updatePermissions() {
const formData = Object.keys(ACCESS_LEVELS).reduce((acc, level) => {
const accessLevelName = ACCESS_LEVELS[level];
const inputData = this[`${accessLevelName}_dropdown`].getInputData(accessLevelName);
acc[`${accessLevelName}_attributes`] = inputData;
return acc;
}, {});
axios
.patch(this.$wrap.data('url'), {
protected_environment: formData,
})
.then(({ data }) => {
this.hasChanges = false;
Object.keys(ACCESS_LEVELS).forEach(level => {
const accessLevelName = ACCESS_LEVELS[level];
// The data coming from server will be the new persisted *state* for each dropdown
this.setSelectedItemsToDropdown(data[accessLevelName], `${accessLevelName}_dropdown`);
});
this.$allowedToDeployDropdown.enable();
})
.catch(() => {
this.$allowedToDeployDropdown.enable();
Flash('Failed to update environment!', null, $('.js-protected-environments-list'));
});
}
setSelectedItemsToDropdown(items = [], dropdownName) {
const itemsToAdd = items.map(currentItem => {
if (currentItem.user_id) {
// Do this only for users for now
// get the current data for selected items
const selectedItems = this[dropdownName].getSelectedItems();
const currentSelectedItem = _.findWhere(selectedItems, {
user_id: currentItem.user_id,
});
return {
id: currentItem.id,
user_id: currentItem.user_id,
type: LEVEL_TYPES.USER,
persisted: true,
name: currentSelectedItem.name,
username: currentSelectedItem.username,
avatar_url: currentSelectedItem.avatar_url,
};
} else if (currentItem.group_id) {
return {
id: currentItem.id,
group_id: currentItem.group_id,
type: LEVEL_TYPES.GROUP,
persisted: true,
};
}
return {
id: currentItem.id,
access_level: currentItem.access_level,
type: LEVEL_TYPES.ROLE,
persisted: true,
};
});
this[dropdownName].setSelectedItems(itemsToAdd);
}
}
/* eslint-disable no-new */
import $ from 'jquery';
import ProtectedEnvironmentEdit from './protected_environment_edit';
export default class ProtectedEnvironmentEditList {
constructor() {
this.$wrap = $('.protected-environments-list');
this.initEditForm();
}
initEditForm() {
this.$wrap.find('.js-protected-environment-edit-form').each((i, el) => {
new ProtectedEnvironmentEdit({
$wrap: $(el),
});
});
}
}
......@@ -5,6 +5,10 @@
}
}
.protected-environments-list {
@extend .protected-branches-list;
}
.project-mirror-settings {
.fingerprint-verified {
color: $green-500;
......
......@@ -6,6 +6,7 @@ module EE
prepended do
before_action :authorize_read_pod_logs!, only: [:logs]
before_action :environment_ee, only: [:logs]
before_action :authorize_create_environment_terminal!, only: [:terminal]
end
def logs
......@@ -31,6 +32,10 @@ module EE
def pod_logs
environment.deployment_platform.read_pod_logs(params[:pod_name])
end
def authorize_create_environment_terminal!
return render_404 unless can?(current_user, :create_environment_terminal, environment)
end
end
end
end
# frozen_string_literal: true
module EE
module Projects
module Settings
module CiCdController
include ::API::Helpers::RelatedResourcesHelpers
extend ::Gitlab::Utils::Override
extend ActiveSupport::Concern
prepended do
before_action :assign_variables_to_gon, only: :show
before_action :define_protected_env_variables, only: :show
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
override :show
......@@ -14,7 +21,24 @@ module EE
super
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
private
def define_protected_env_variables
@protected_environments = @project.protected_environments.order(:name)
@protected_environments_count = @protected_environments.count
@protected_environment = @project.protected_environments.new
end
def assign_variables_to_gon
gon.push(current_project_id: project.id)
gon.push(deploy_access_levels: environment_dropdown.roles_hash)
gon.push(search_unprotected_environments_url: search_project_protected_environments_path(@project))
end
def environment_dropdown
@environment_dropdown ||= ProtectedEnvironments::EnvironmentDropdownService
end
end
end
end
......
# frozen_string_literal: true
class Projects::ProtectedEnvironmentsController < Projects::ApplicationController
before_action :authorize_admin_project!
before_action :protected_environment, except: [:create, :search]
def create
protected_environment = ::ProtectedEnvironments::CreateService.new(@project, current_user, protected_environment_params).execute
if protected_environment.persisted?
flash[:notice] = s_('ProtectedEnvironment|Your environment has been protected.')
else
flash[:alert] = protected_environment.errors.full_messages.join(', ')
end
redirect_to project_settings_ci_cd_path(@project)
end
def update
result = ::ProtectedEnvironments::UpdateService.new(@project, current_user, protected_environment_params).execute(@protected_environment)
if result
render json: @protected_environment, status: :ok, include: :deploy_access_levels
else
render json: @protected_environment.errors, status: :unprocessable_entity
end
end
def destroy
result = ::ProtectedEnvironments::DestroyService.new(@project, current_user).execute(@protected_environment)
if result
flash[:notice] = s_('ProtectedEnvironment|Your environment has been unprotected')
else
flash[:alert] = s_("ProtectedEnvironment|Your environment can't be unprotected")
end
redirect_to project_settings_ci_cd_path(@project), status: :found
end
def search
unprotected_environment_names = ::ProtectedEnvironments::SearchService.new(@project, current_user).execute(search_params[:query])
render json: unprotected_environment_names, status: :ok
end
private
def protected_environment
@protected_environment = @project.protected_environments.find(params[:id])
end
def protected_environment_params
params.require(:protected_environment).permit(:name, deploy_access_levels_attributes: deploy_access_level_attributes)
end
def deploy_access_level_attributes
%i(access_level id user_id _destroy group_id)
end
def search_params
params.permit(:query)
end
end
module ProtectedEnvironmentsHelper
def protected_environments_enabled?
Feature.enabled?('protected_environments')
end
end
# frozen_string_literal: true
module EE
module CommitStatus
EE_FAILURE_REASONS = {
protected_environment_failure: 1_000
}.freeze
end
end
module EE
module Environment
extend ActiveSupport::Concern
include ::Gitlab::Utils::StrongMemoize
prepended do
has_many :prometheus_alerts, inverse_of: :environment
......@@ -21,5 +22,13 @@ module EE
def cluster_prometheus_adapter
@cluster_prometheus_adapter ||= Prometheus::AdapterService.new(project, deployment_platform).cluster_prometheus_adapter
end
def protected?
project.protected_environment_by_name(name).present?
end
def protected_deployable_by_user?(user)
project.protected_environment_accessible_to?(name, user)
end
end
end
......@@ -35,6 +35,7 @@ module EE
has_many :audit_events, as: :entity
has_many :path_locks
has_many :vulnerability_feedback
has_many :protected_environments
has_many :software_license_policies, inverse_of: :project, class_name: 'SoftwareLicensePolicy'
accepts_nested_attributes_for :software_license_policies, allow_destroy: true
......@@ -503,6 +504,18 @@ module EE
end
request_cache(:any_path_locks?) { self.id }
def protected_environment_accessible_to?(environment_name, user)
protected_environment = protected_environment_by_name(environment_name)
!protected_environment || protected_environment.accessible_to?(user)
end
def protected_environment_by_name(environment_name)
return nil unless protected_environments_feature_available?
protected_environments.find_by(name: environment_name)
end
override :after_import
def after_import
super
......@@ -519,6 +532,10 @@ module EE
::Gitlab::CurrentSettings.custom_project_templates_enabled?
end
def protected_environments_feature_available?
Feature.enabled?('protected_environments') && feature_available?(:protected_environments)
end
private
def set_override_pull_mirror_available
......
......@@ -61,6 +61,7 @@ class License < ActiveRecord::Base
commit_committer_check
external_authorization_service
ci_cd_projects
protected_environments
system_header_footer
custom_project_templates
].freeze
......
# frozen_string_literal: true
class ProtectedEnvironment < ActiveRecord::Base
include ::Gitlab::Utils::StrongMemoize
belongs_to :project
has_many :deploy_access_levels, inverse_of: :protected_environment
accepts_nested_attributes_for :deploy_access_levels, allow_destroy: true
validates :deploy_access_levels, length: { minimum: 1 }
validates :name, :project, presence: true
def accessible_to?(user)
deploy_access_levels
.any? { |deploy_access_level| deploy_access_level.check_access(user) }
end
end
# frozen_string_literal: true
class ProtectedEnvironment::DeployAccessLevel < ActiveRecord::Base
ALLOWED_ACCESS_LEVELS = [
Gitlab::Access::MAINTAINER,
Gitlab::Access::DEVELOPER,
Gitlab::Access::ADMIN
].freeze
HUMAN_ACCESS_LEVELS = {
Gitlab::Access::MAINTAINER => 'Maintainers'.freeze,
Gitlab::Access::DEVELOPER => 'Developers + Maintainers'.freeze
}.freeze
belongs_to :user
belongs_to :group
belongs_to :protected_environment
validates :access_level, presence: true, if: :role?, inclusion: {
in: ALLOWED_ACCESS_LEVELS
}
delegate :project, to: :protected_environment
def check_access(user)
return true if user.admin?
return user.id == user_id if user_type?
return group.users.exists?(user.id) if group_type?
project.team.max_member_access(user.id) >= access_level
end
def user_type?
user_id.present?
end
def group_type?
group_id.present?
end
def type
if user_type?
:user
elsif group_type?
:group
else
:role
end
end
def role?
type == :role
end
def humanize
return user.name if user_type?
return group.name if group_type?
HUMAN_ACCESS_LEVELS[access_level]
end
end
# frozen_string_literal: true
module EE
module Ci
module BuildPolicy
extend ActiveSupport::Concern
prepended do
condition(:deployable_by_user) { deployable_by_user? }
rule { ~deployable_by_user }.policy do
prevent :update_build
end
private
alias_method :current_user, :user
alias_method :build, :subject
def deployable_by_user?
# This feature flag is used here as evaluating `build.expanded_environment_name` is expensive
return true unless build.project.protected_environments_feature_available?
build.project.protected_environment_accessible_to?(build.expanded_environment_name, user)
end
end
end
end
end
# frozen_string_literal: true
module EE
module EnvironmentPolicy
extend ActiveSupport::Concern
prepended do
condition(:deployable_by_user) { deployable_by_user? }
rule { ~deployable_by_user }.policy do
prevent :stop_environment
prevent :create_environment_terminal
end
private
alias_method :current_user, :user
alias_method :environment, :subject
def deployable_by_user?
environment.protected_deployable_by_user?(current_user)
end
end
end
end
# frozen_string_literal: true
module EE
module CommitStatusPresenter
EE_CALLOUT_FAILURE_MESSAGES = {
protected_environment_failure: 'The environment this job is deploying to is protected. Only users with permission may successfully run this job'
}.freeze
end
end
module EE
module EnvironmentEntity
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
prepended do
expose :logs_path, if: -> (*) { can_read_pod_logs? } do |environment|
......@@ -8,6 +9,8 @@ module EE
end
end
private
def can_read_pod_logs?
can?(current_user, :read_pod_logs, environment.project)
end
......
# frozen_string_literal: true
module EE
module Ci
module EnqueueBuildService
extend ::Gitlab::Utils::Override
override :execute
def execute(build)
unless allowed_to_deploy?(build)
return build.drop!(:protected_environment_failure)
end
super
end
private
def allowed_to_deploy?(build)
# This feature flag is used here as evaluating `build.expanded_environment_name` is expensive
return true unless project.protected_environments_feature_available?
project.protected_environment_accessible_to?(build.expanded_environment_name, build.user)
end
end
end
end
# frozen_string_literal: true
module ProtectedEnvironments
class CreateService < BaseService
def execute
project.protected_environments.create(params)
end
end
end
# frozen_string_literal: true
module ProtectedEnvironments
class DestroyService < BaseService
def execute(protected_environment)
protected_environment.destroy
end
end
end
# frozen_string_literal: true
module ProtectedEnvironments
class EnvironmentDropdownService
def self.roles_hash
{ roles: roles }
end
def self.roles
human_access_levels.map do |id, text|
{ id: id, text: text, before_divider: true }
end
end
def self.human_access_levels
::ProtectedEnvironment::DeployAccessLevel::HUMAN_ACCESS_LEVELS
end
end
end
# frozen_string_literal: true
module ProtectedEnvironments
class SearchService < BaseService
# Returns unprotected environments filtered by name
# Limited to 20 per performance reasons
def execute(name)
project
.environments
.where.not(name: project.protected_environments.select(:name))
.where('environments.name LIKE ?', "#{name}%")
.order_by_last_deployed_at
.limit(20)
.pluck(:name)
end
end
end
# frozen_string_literal: true
module ProtectedEnvironments
class UpdateService < BaseService
def execute(protected_environment)
protected_environment.update(params)
end
end
end
.deploy_access_levels-container
= dropdown_tag(s_('ProtectedEnvironment|Choose who is allowed to deploy'),
options: { toggle_class: 'js-allowed-to-deploy wide js-multiselect',
dropdown_class: 'dropdown-menu-user dropdown-menu-selectable',
filter: true,
data: { field_name: 'protected_environments[deploy_access_levels_attributes][0][access_level]', input_id: 'deploy_access_levels_attributes' }})
= f.hidden_field(:name)
= dropdown_tag(s_('ProtectedEnvironment|Select an environment'),
options: { toggle_class: 'js-protected-environment-select js-filter-submit wide',
filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search environments',
footer_content: true,
data: { show_no: true, show_any: true, show_upcoming: true,
selected: params[:protected_environment_name],
project_id: project.id } })
.protected-environments-list.js-protected-environments-list
- if @protected_environments.empty?
%p.settings-message.text-center
= s_('ProtectedEnvironment|There are currently no protected environments, protect an environment with the form above.')
- else
%table.table.table-bordered
%colgroup
%col{ width: '30%' }
%col
- if can_admin_project
%col{ width: '10%' }
%thead
%tr
%th= s_('ProtectedEnvironment|Protected Environment (%{protected_environments_count})') % { protected_environments_count: @protected_environments_count }
%th= s_('ProtectedEnvironment|Allowed to deploy')
- if can_admin_project
%th
%tbody
= render partial: 'projects/protected_environments/protected_environment', collection: @protected_environments, locals: { can_admin_project: can_admin_project }
= form_for [@project.namespace.becomes(Namespace), @project, @protected_environment], html: { class: 'new-protected-environment js-new-protected-environment' } do |f|
.card
.card-header
%h3.card-title
= s_('ProtectedEnvironment|Protect an environment')
.card-body
= form_errors(@protected_environment)
.form-group
= f.label :name, class: 'label-bold' do
= s_('ProtectedEnvironment|Environment')
= render partial: 'projects/protected_environments/environments_dropdown', locals: { f: f, project: @project }
.form-group
= f.label :deploy_access_levels_attributes, class: 'label-bold' do
= s_('ProtectedEnvironment|Allowed to deploy')
= render partial: 'projects/protected_environments/deploy_access_levels_dropdown', locals: { f: f }
.card-footer
= f.submit s_('ProtectedEnvironment|Protect'), class: 'btn-create btn', disabled: true
%tr.js-protected-environment-edit-form{ data: { url: namespace_project_protected_environment_path(@project.namespace, @project, protected_environment) } }
%td
%span.ref-name= link_to protected_environment.name, namespace_project_environment_path(@project.namespace, @project, protected_environment.name)
%td
= render partial: 'projects/protected_environments/update_deploy_access_level_dropdown', locals: { protected_environment: protected_environment, access_levels: protected_environment.deploy_access_levels, disabled: !can_admin_project }
- if can_admin_project
%td
= link_to s_('ProtectedEnvironment|Unprotect'), [@project.namespace.becomes(Namespace), @project, protected_environment], disabled: local_assigns[:disabled], data: { confirm: s_('ProtectedEnvironment|%{environment_name} will be writable for developers. Are you sure?') % { environment_name: protected_environment.name } }, method: :delete, class: 'btn btn-warning'
- default_label = s_('RepositorySettingsAccessLevel|Select')
.deploy_access_levels-container
= dropdown_tag(default_label, options: { toggle_class: 'js-allowed-to-deploy wide js-multiselect', disabled: local_assigns[:disabled], dropdown_class: 'dropdown-menu-selectable dropdown-menu-user', filter: true, data: { field_name: "allowed_to_deploy_#{protected_environment.id}", preselected_items: access_levels_data(access_levels) }})
- expanded = Rails.env.test?
- can_admin_project = can?(current_user, :admin_project, @project)
- if protected_environments_enabled?
%section.protected-environments-settings.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Protected Environments')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
= s_('ProtectedEnvironment|Protect Environments in order to restrict who can execute deployments.')
.settings-content
= render 'projects/protected_environments/form'
.settings-content
= render partial: 'projects/protected_environments/environments_list', locals: { can_admin_project: can_admin_project }
---
title: Add backend structure for ProtectedEnvironments
merge_request: 6672
author:
type: added
# frozen_string_literal: true
class AddProtectedEnvironmentsTable < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :protected_environments do |t|
t.references :project, null: false, index: true, foreign_key: { on_delete: :cascade }
t.timestamps_with_timezone null: false
t.string :name, null: false
end
add_index :protected_environments, [:project_id, :name], unique: true
end
end
# frozen_string_literal: true
class AddProtectedEnvironmentDeployAccessLevelTable < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
GITLAB_ACCESS_MAINTAINER = 40
def up
create_table :protected_environment_deploy_access_levels do |t|
t.timestamps_with_timezone null: false
t.integer :access_level, default: GITLAB_ACCESS_MAINTAINER, null: true
t.references :protected_environment, index: { name: 'index_protected_environment_deploy_access' }, foreign_key: { on_delete: :cascade }, null: false
t.references :user, index: true, foreign_key: { on_delete: :cascade }
t.references :group, references: :namespace, column: :group_id, index: true
t.foreign_key :namespaces, column: :group_id, on_delete: :cascade
end
end
def down
if foreign_keys_for(:protected_environment_deploy_access_levels, :group_id).any?
remove_foreign_key :protected_environment_deploy_access_levels, column: :group_id
end
if foreign_keys_for(:protected_environment_deploy_access_levels, :user_id).any?
remove_foreign_key :protected_environment_deploy_access_levels, column: :user_id
end
drop_table :protected_environment_deploy_access_levels
end
end
class AddIndexToEnvironmentNameForLike < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_environments_on_name_varchar_pattern_ops'
disable_ddl_transaction!
def up
return unless Gitlab::Database.postgresql?
unless index_exists?(:environments, :name, name: INDEX_NAME)
execute("CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON environments (name varchar_pattern_ops);")
end
end
def down
return unless Gitlab::Database.postgresql?
return unless index_exists?(:environments, :name, name: INDEX_NAME)
remove_concurrent_index_by_name(:environments, INDEX_NAME)
end
end
# frozen_string_literal: true
module EE
module Gitlab
module Ci
module Status
module Build
module Failed
EE_REASONS = {
protected_environment_failure: 'protected environment failure'
}.freeze
end
end
end
end
end
end
module EE
module Gitlab
module ImportExport
module RelationFactory
extend ActiveSupport::Concern
EE_OVERRIDES = {
deploy_access_levels: 'ProtectedEnvironment::DeployAccessLevel',
unprotect_access_levels: 'ProtectedBranch::UnprotectAccessLevel'
}.freeze
class_methods do
extend ::Gitlab::Utils::Override
override :overrides
def overrides
super.merge(EE_OVERRIDES)
end
end
end
end
end
end
......@@ -120,6 +120,49 @@ describe Projects::EnvironmentsController do
end
end
describe '#GET terminal' do
let(:protected_environment) { create(:protected_environment, name: environment.name, project: project) }
before do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:protected_environments).and_return(true)
end
context 'when environment is protected' do
context 'when user does not have access to it' do
before do
protected_environment
get :terminal, environment_params
end
it 'should response with access denied' do
expect(response).to have_gitlab_http_status(404)
end
end
context 'when user has access to it' do
before do
protected_environment.deploy_access_levels.create(user: user)
get :terminal, environment_params
end
it 'should be successful' do
expect(response).to have_gitlab_http_status(200)
end
end
end
context 'when environment is not protected' do
it 'should be successful' do
get :terminal, environment_params
expect(response).to have_gitlab_http_status(200)
end
end
end
def environment_params(opts = {})
opts.reverse_merge(namespace_id: project.namespace,
project_id: project,
......
# frozen_string_literal: true
require 'spec_helper'
describe Projects::ProtectedEnvironmentsController do
let(:project) { create(:project) }
let(:current_user) { create(:user) }
let(:maintainer_access) { Gitlab::Access::MAINTAINER }
before do
sign_in(current_user)
end
describe '#POST create' do
let(:params) do
attributes_for(:protected_environment,
deploy_access_levels_attributes: [{ access_level: maintainer_access }])
end
subject do
post :create,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
protected_environment: params
end
context 'with valid access and params' do
before do
project.add_maintainer(current_user)
end
context 'with valid params' do
it 'should create a new ProtectedEnvironment' do
expect do
subject
end.to change(ProtectedEnvironment, :count).by(1)
end
it 'should set a flash' do
subject
expect(controller).to set_flash[:notice].to(/environment has been protected/)
end
it 'should redirect to CI/CD settings' do
subject
expect(response).to redirect_to project_settings_ci_cd_path(project)
end
end
context 'with invalid params' do
let(:params) do
attributes_for(:protected_environment,
name: '',
deploy_access_levels_attributes: [{ access_level: maintainer_access }])
end
it 'should not create a new ProtectedEnvironment' do
expect do
subject
end.not_to change(ProtectedEnvironment, :count)
end
it 'should redirect to CI/CD settings' do
subject
expect(response).to redirect_to project_settings_ci_cd_path(project)
end
end
end
context 'with invalid access' do
before do
project.add_developer(current_user)
end
it 'should render 404' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
end
describe '#PUT update' do
let(:protected_environment) { create(:protected_environment, project: project) }
let(:deploy_access_level) { protected_environment.deploy_access_levels.first }
let(:params) do
{
deploy_access_levels_attributes: [
{ id: deploy_access_level.id, access_level: Gitlab::Access::DEVELOPER },
{ access_level: maintainer_access }
]
}
end
subject do
put :update,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: protected_environment.id,
protected_environment: params
end
context 'when the user is authorized' do
before do
project.add_maintainer(current_user)
subject
end
it 'should find the requested protected environment' do
expect(assigns(:protected_environment)).to eq(protected_environment)
end
it 'should update the protected environment' do
expect(protected_environment.deploy_access_levels.count).to eq(2)
end
it 'should be success' do
expect(response).to have_gitlab_http_status(200)
end
end
context 'when the user is not authorized' do
before do
project.add_developer(current_user)
subject
end
it 'should not be success' do
expect(response).to have_gitlab_http_status(404)
end
end
end
describe '#DELETE destroy' do
let!(:protected_environment) { create(:protected_environment, project: project) }
subject do
delete :destroy,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: protected_environment.id
end
context 'when the user is authorized' do
before do
project.add_maintainer(current_user)
end
it 'should find the requested protected environment' do
subject
expect(assigns(:protected_environment)).to eq(protected_environment)
end
it 'should delete the requested protected environment' do
expect do
subject
end.to change { ProtectedEnvironment.count }.from(1).to(0)
end
it 'should redirect to CI/CD settings' do
subject
expect(response).to redirect_to project_settings_ci_cd_path(project)
end
end
context 'when the user is not authorized' do
before do
project.add_developer(current_user)
end
it 'should not be success' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :ee_ci_build, class: Ci::Build, parent: :ci_build do
trait :protected_environment_failure do
failed
failure_reason { Ci::Build.failure_reasons[:protected_environment_failure] }
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :protected_environment do
name 'production'
project
transient do
authorize_user_to_deploy nil
authorize_group_to_deploy nil
end
after(:build) do |protected_environment, evaluator|
if user = evaluator.authorize_user_to_deploy
protected_environment.deploy_access_levels.new(user: user)
end
if group = evaluator.authorize_group_to_deploy
protected_environment.deploy_access_levels.new(group: group)
end
if protected_environment.deploy_access_levels.empty?
protected_environment.deploy_access_levels.new(user: create(:user))
end
end
trait :maintainers_can_deploy do
after(:build) do |protected_environment|
protected_environment.deploy_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
end
end
trait :developers_can_deploy do
after(:build) do |protected_environment|
protected_environment.deploy_access_levels.new(access_level: Gitlab::Access::DEVELOPER)
end
end
trait :staging do
name 'staging'
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :protected_environment_deploy_access_level, class: ProtectedEnvironment::DeployAccessLevel do
user nil
group nil
protected_environment
access_level { Gitlab::Access::DEVELOPER }
trait :maintainer_access do
access_level { Gitlab::Access::MAINTAINER }
end
end
end
......@@ -24,4 +24,93 @@ describe Environment do
end
end
end
describe '#protected?' do
subject { environment.protected? }
before do
stub_feature_flags(protected_environments: enabled)
end
context 'when Protected Environments feature is not on' do
let(:enabled) { false }
it { is_expected.to be_falsy }
end
context 'when Protected Environments feature is on' do
let(:enabled) { true }
context 'when Protected Environments feature is not available in the project' do
it { is_expected.to be_falsy }
end
context 'when Protected Environments feature is available in the project' do
before do
allow(project).to receive(:feature_available?)
.with(:protected_environments).and_return(true)
end
context 'when the environment is protected' do
before do
create(:protected_environment, name: environment.name, project: project)
end
it { is_expected.to be_truthy }
end
context 'when the environment is not protected' do
it { is_expected.to be_falsy }
end
end
end
end
describe '#protected_deployable_by_user?' do
let(:user) { create(:user) }
let(:protected_environment) { create(:protected_environment, :maintainers_can_deploy, name: environment.name, project: project) }
subject { environment.protected_deployable_by_user?(user) }
before do
stub_feature_flags(protected_environments: enabled)
end
context 'when Protected Environments feature is not on' do
let(:enabled) { false }
it { is_expected.to be_truthy }
end
context 'when Protected Environments feature is on' do
let(:enabled) { true }
context 'when Protected Environments feature is available in the project' do
before do
allow(project).to receive(:feature_available?)
.with(:protected_environments).and_return(true)
end
context 'when the environment is not protected' do
it { is_expected.to be_truthy }
end
context 'when environment is protected and user dont have access to it' do
before do
protected_environment
end
it { is_expected.to be_falsy }
end
context 'when environment is protected and user have access to it' do
before do
protected_environment.deploy_access_levels.create(user: user)
end
it { is_expected.to be_truthy }
end
end
end
end
end
......@@ -21,6 +21,7 @@ describe Project do
it { is_expected.to have_many(:sourced_pipelines) }
it { is_expected.to have_many(:source_pipelines) }
it { is_expected.to have_many(:audit_events).dependent(false) }
it { is_expected.to have_many(:protected_environments) }
end
describe 'validations' do
......@@ -1529,6 +1530,117 @@ describe Project do
end
end
describe '#protected_environment_by_name' do
let(:project) { create(:project) }
subject { project.protected_environment_by_name('production') }
before do
stub_feature_flags(protected_environments: enabled)
end
context 'when Protected Environments feature is not on' do
let(:enabled) { false }
it { is_expected.to be_nil }
context 'when Protected Environments feature is available on the project' do
before do
allow(project).to receive(:feature_available?)
.with(:protected_environments).and_return(true)
end
it { is_expected.to be_nil }
end
end
context 'when Protected Environments feature is on' do
let(:enabled) { true }
context 'when Protected Environments feature is not available on the project' do
it { is_expected.to be_nil }
end
context 'when Protected Environments feature is available on the project' do
let(:environment) { create(:environment, name: 'production') }
let(:protected_environment) { create(:protected_environment, name: environment.name, project: project) }
before do
allow(project).to receive(:feature_available?)
.with(:protected_environments).and_return(true)
end
context 'when the project environment exists' do
before do
protected_environment
end
it { is_expected.to eq(protected_environment) }
end
context 'when the project environment does not exists' do
it { is_expected.to be_nil }
end
end
end
end
describe '#protected_environment_accessible_to?' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:environment) { create(:environment, project: project) }
let(:protected_environment) { create(:protected_environment, project: project, name: environment.name) }
subject { project.protected_environment_accessible_to?(environment.name, user) }
before do
stub_feature_flags(protected_environments: enabled)
end
context 'when Protected Environments feature is not on' do
let(:enabled) { false }
it { is_expected.to be_truthy }
end
context 'when Protected Environments feature is on' do
let(:enabled) { true }
context 'when Protected Environments feature is not available on the project' do
it { is_expected.to be_truthy }
end
context 'when Protected Environments feature is available on the project' do
before do
allow(project).to receive(:feature_available?)
.with(:protected_environments).and_return(true)
end
context 'when project does not have protected environments' do
it { is_expected.to be_truthy }
end
context 'when project has protected environments' do
context 'when user has the right access' do
before do
protected_environment.deploy_access_levels.create(user_id: user.id)
end
it { is_expected.to be_truthy }
end
context 'when user does not have the right access' do
before do
protected_environment.deploy_access_levels.create
end
it { is_expected.to be_falsy }
end
end
end
end
end
describe '#after_import' do
let(:project) { create(:project) }
let(:repository_updated_service) { instance_double('::Geo::RepositoryUpdatedService') }
......
# frozen_string_literal: true
require 'spec_helper'
describe ProtectedEnvironment::DeployAccessLevel do
describe 'associations' do
it { is_expected.to belong_to(:protected_environment) }
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:group) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:access_level) }
end
describe '#check_access' do
let(:project) { create(:project) }
let(:protected_environment) { create(:protected_environment, project: project) }
let(:user) { create(:user) }
subject { deploy_access_level.check_access(user) }
describe 'admin access' do
let(:user) { create(:user, :admin) }
context 'when admin user does have specific access' do
let(:deploy_access_level) { create(:protected_environment_deploy_access_level, protected_environment: protected_environment, user: user) }
it { is_expected.to be_truthy }
end
context 'when admin user does not have specific access' do
let(:deploy_access_level) { create(:protected_environment_deploy_access_level, protected_environment: protected_environment) }
it { is_expected.to be_truthy }
end
end
describe 'user access' do
context 'when specific access has been assigned to a user' do
let(:deploy_access_level) { create(:protected_environment_deploy_access_level, protected_environment: protected_environment, user: user) }
it { is_expected.to be_truthy }
end
context 'when no permissions have been given to a user' do
let(:deploy_access_level) { create(:protected_environment_deploy_access_level, protected_environment: protected_environment) }
it { is_expected.to be_falsy }
end
end
describe 'group access' do
let(:group) { create(:group, projects: [project]) }
context 'when specific access has been assigned to a group' do
let(:deploy_access_level) { create(:protected_environment_deploy_access_level, protected_environment: protected_environment, group: group) }
before do
group.add_reporter(user)
end
it { is_expected.to be_truthy }
end
context 'when no permissions have been given to a group' do
let(:deploy_access_level) { create(:protected_environment_deploy_access_level, protected_environment: protected_environment) }
before do
group.add_reporter(user)
end
it { is_expected.to be_falsy }
end
end
describe 'access level' do
let(:developer_access) { Gitlab::Access::DEVELOPER }
let(:deploy_access_level) { create(:protected_environment_deploy_access_level, protected_environment: protected_environment, access_level: developer_access) }
context 'when user is project member above the permitted access level' do
before do
project.add_developer(user)
end
it { is_expected.to be_truthy }
end
context 'when user is project member below the permitted access level' do
before do
project.add_reporter(user)
end
it { is_expected.to be_falsy }
end
end
end
describe '#humanize' do
let(:protected_environment) { create(:protected_environment) }
subject { deploy_access_level.humanize }
context 'when is related to a user' do
let(:user) { create(:user) }
let(:deploy_access_level) { create(:protected_environment_deploy_access_level, protected_environment: protected_environment, user: user) }
it { is_expected.to eq(user.name) }
end
context 'when is related to a group' do
let(:group) { create(:group) }
let(:deploy_access_level) { create(:protected_environment_deploy_access_level, protected_environment: protected_environment, group: group) }
it { is_expected.to eq(group.name) }
end
context 'when is set to have a role' do
let(:deploy_access_level) { create(:protected_environment_deploy_access_level, protected_environment: protected_environment, access_level: access_level) }
context 'for developer access' do
let(:access_level) { Gitlab::Access::DEVELOPER }
it { is_expected.to eq('Developers + Maintainers') }
end
context 'for maintainer access' do
let(:access_level) { Gitlab::Access::MAINTAINER }
it { is_expected.to eq('Maintainers') }
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ProtectedEnvironment do
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:deploy_access_levels) }
end
describe 'validation' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_length_of(:deploy_access_levels) }
end
describe '#accessible_to?' do
let(:project) { create(:project) }
let(:environment) { create(:environment, project: project) }
let(:protected_environment) { create(:protected_environment, name: environment.name, project: project) }
let(:user) { create(:user) }
subject { protected_environment.accessible_to?(user) }
context 'when user is admin' do
let(:user) { create(:user, :admin) }
it { is_expected.to be_truthy }
end
context 'when access has been granted to user' do
before do
create_deploy_access_level(user: user)
end
it { is_expected.to be_truthy }
end
context 'when specific access has been assigned to a group' do
let(:group) { create(:group) }
before do
create_deploy_access_level(group: group)
end
it 'should allow members of the group' do
group.add_developer(user)
expect(subject).to be_truthy
end
it 'should reject non-members of the group' do
expect(subject).to be_falsy
end
end
context 'when access has been granted to maintainers' do
before do
create_deploy_access_level(access_level: Gitlab::Access::MAINTAINER)
end
it 'should allow maintainers' do
project.add_maintainer(user)
expect(subject).to be_truthy
end
it 'should reject developers' do
project.add_developer(user)
expect(subject).to be_falsy
end
end
context 'when access has been granted to developers' do
before do
create_deploy_access_level(access_level: Gitlab::Access::DEVELOPER)
end
it 'should allow maintainers' do
project.add_maintainer(user)
expect(subject).to be_truthy
end
it 'should allow developers' do
project.add_developer(user)
expect(subject).to be_truthy
end
end
end
def create_deploy_access_level(**opts)
protected_environment.deploy_access_levels.create(**opts)
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Ci::BuildPolicy do
using RSpec::Parameterized::TableSyntax
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
describe '#update_build?' do
let(:environment) { create(:environment, project: project, name: 'production') }
let(:build) { create(:ee_ci_build, pipeline: pipeline, environment: 'production', ref: 'development') }
subject { user.can?(:update_build, build) }
it_behaves_like 'protected environments access'
end
end
# frozen_string_literal: true
require 'spec_helper'
describe EnvironmentPolicy do
using RSpec::Parameterized::TableSyntax
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:environment) { create(:environment, :with_review_app, ref: 'development', project: project) }
before do
project.repository.add_branch(user, 'development', project.commit.id)
end
describe '#stop_environment' do
subject { user.can?(:stop_environment, environment) }
it_behaves_like 'protected environments access'
end
describe '#create_environment_terminal' do
subject { user.can?(:create_environment_terminal, environment) }
it_behaves_like 'protected environments access', false
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Ci::BuildPresenter do
subject(:presenter) { described_class.new(build) }
describe '#callout_failure_message' do
let(:build) { create(:ee_ci_build, :protected_environment_failure) }
it 'returns a verbose failure reason' do
description = presenter.callout_failure_message
expect(description).to eq('The environment this job is deploying to is protected. Only users with permission may successfully run this job')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe EnvironmentEntity do
using RSpec::Parameterized::TableSyntax
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:environment) { create(:environment, :with_review_app, ref: 'development', project: project) }
let(:protected_environment) { create(:protected_environment, name: environment.name, project: project) }
let(:entity) do
described_class.new(environment, request: double(current_user: user, project: project))
end
before do
project.repository.add_branch(user, 'development', project.commit.id)
end
describe '#can_stop' do
subject { entity.as_json[:can_stop] }
it_behaves_like 'protected environments access'
end
describe '#terminal_path' do
subject { entity.as_json.include?(:terminal_path) }
before do
allow(environment).to receive(:has_terminals?).and_return(true)
end
it_behaves_like 'protected environments access', false
end
end
# frozen_string_literal: true
require 'spec_helper'
describe JobEntity do
using RSpec::Parameterized::TableSyntax
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:request) { double('request') }
let(:entity) { described_class.new(job, request: request) }
let(:environment) { create(:environment, project: project) }
before do
allow(request).to receive(:current_user).and_return(user)
end
describe '#playable?' do
let(:job) { create(:ci_build, :manual, project: project, environment: environment.name, ref: 'development') }
subject { entity.as_json[:playable] }
it_behaves_like 'protected environments access'
end
describe '#retryable?' do
let(:job) { create(:ci_build, :failed, project: project, environment: environment.name, ref: 'development') }
subject { entity.as_json.include?(:retry_path) }
it_behaves_like 'protected environments access'
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Ci::EnqueueBuildService, '#execute' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:environment) { create(:environment, project: project, name: 'production') }
let(:protected_environment) { create(:protected_environment, name: environment.name, project: project) }
let(:ci_build) { create(:ci_build, :created, environment: environment.name, user: user) }
subject { described_class.new(project, user).execute(ci_build) }
before do
stub_feature_flags(protected_environments: enabled)
end
context 'when related to a protected environment' do
context 'when Protected Environments feature is not on' do
let(:enabled) { false }
it 'enqueues the build' do
subject
expect(ci_build.pending?).to be_truthy
end
end
context 'when Protected Environments feature is on' do
let(:enabled) { true }
context 'when Protected Environments feature is available in the project' do
before do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?)
.with(:protected_environments).and_return(true)
protected_environment
end
context 'when user does not have access to the environment' do
it 'should fail the build' do
subject
expect(ci_build.failed?).to be_truthy
expect(ci_build.failure_reason).to eq('protected_environment_failure')
end
end
context 'when user has access to the environment' do
before do
protected_environment.deploy_access_levels.create(user: user)
end
it 'enqueues the build' do
subject
expect(ci_build.pending?).to be_truthy
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Ci::PlayBuildService, '#execute' do
it_behaves_like 'restricts access to protected environments'
end
# frozen_string_literal: true
require 'spec_helper'
describe Ci::RetryBuildService do
it_behaves_like 'restricts access to protected environments'
end
# frozen_string_literal: true
require 'spec_helper'
describe ProtectedEnvironments::CreateService, '#execute' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:maintainer_access) { Gitlab::Access::MAINTAINER }
let(:params) do
attributes_for(:protected_environment,
deploy_access_levels_attributes: [{ access_level: maintainer_access }])
end
subject { described_class.new(project, user, params).execute }
context 'with valid params' do
it { is_expected.to be_truthy }
it 'should create a record on ProtectedEnvironment' do
expect { subject }.to change(ProtectedEnvironment, :count).by(1)
end
it 'should create a record on ProtectedEnvironment record' do
expect { subject }.to change(ProtectedEnvironment::DeployAccessLevel, :count).by(1)
end
end
context 'with invalid params' do
let(:maintainer_access) { 0 }
it 'should return a non persisted Protected Environment record' do
expect(subject.persisted?).to be_falsy
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ProtectedEnvironments::DestroyService, '#execute' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let!(:protected_environment) { create(:protected_environment, project: project) }
let(:deploy_access_level) { protected_environment.deploy_access_levels.first }
subject { described_class.new(project, user).execute(protected_environment) }
context 'when the Protected Environment is deleted' do
it { is_expected.to be_truthy }
it 'should delete the requested ProtectedEnvironment' do
expect do
subject
end.to change { ProtectedEnvironment.count }.from(1).to(0)
end
it 'should delete the related DeployAccessLevel' do
expect do
subject
end.to change { ProtectedEnvironment::DeployAccessLevel.count }.from(1).to(0)
end
end
context 'when the Protected Environment can not be deleted' do
before do
allow(protected_environment).to receive(:destroy).and_return(false)
end
it { is_expected.to be_falsy }
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ProtectedEnvironments::EnvironmentDropdownService, '#roles' do
let(:roles) do
[
{ id: 40, text: 'Maintainers', before_divider: true },
{ id: 30, text: 'Developers + Maintainers', before_divider: true }
]
end
subject { described_class.roles_hash }
describe '#roles' do
it 'returns a hash with access levels for allowed to deploy option' do
expect(subject[:roles]).to match_array(roles)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ProtectedEnvironments::SearchService, '#execute' do
let(:project) { create(:project) }
let(:user) { create(:user) }
subject { described_class.new(project, user).execute(environment_name) }
before do
%w(production staging review/app_1 review/app_2 test canary).each do |environment_name|
create(:environment, name: environment_name, project: project)
end
create(:protected_environment, name: 'production', project: project)
create(:protected_environment, name: 'staging', project: project)
end
context 'with empty search' do
let(:environment_name) { '' }
it 'returns unfiltered unprotected environments' do
unprotected_environments = %w(review/app_1 review/app_2 test canary)
expect(subject).to match_array(unprotected_environments)
end
end
context 'with specific search' do
let(:environment_name) { 'review' }
it 'returns specific unprotected environemtns' do
expect(subject).to match_array(['review/app_1', 'review/app_2'])
end
end
context 'when no match' do
let(:environment_name) { 'no_match' }
it 'should return an empty array' do
expect(subject).to eq([])
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ProtectedEnvironments::UpdateService, '#execute' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:maintainer_access) { Gitlab::Access::MAINTAINER }
let(:protected_environment) { create(:protected_environment, project: project) }
let(:deploy_access_level) { protected_environment.deploy_access_levels.first }
let(:params) do
{
deploy_access_levels_attributes: [
{ id: deploy_access_level.id, access_level: Gitlab::Access::DEVELOPER },
{ access_level: maintainer_access }
]
}
end
subject { described_class.new(project, user, params).execute(protected_environment) }
before do
deploy_access_level
end
context 'with valid params' do
it { is_expected.to be_truthy }
it 'should change update the deploy access levels' do
expect do
subject
end.to change { ProtectedEnvironment::DeployAccessLevel.count }.from(1).to(2)
end
end
context 'with invalid params' do
let(:maintainer_access) { 0 }
it { is_expected.to be_falsy }
it 'should not update the deploy access levels' do
expect do
subject
end.not_to change { ProtectedEnvironment::DeployAccessLevel.count }
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'protected environments access' do |developer_access = true|
using RSpec::Parameterized::TableSyntax
before do
stub_feature_flags(protected_environments: enabled)
end
context 'when Protected Environments feature is not on' do
let(:enabled) { false }
where(:access_level, :result) do
:guest | false
:reporter | false
:developer | developer_access
:maintainer | true
:admin | true
end
with_them do
before do
environment
update_user_access(access_level, user, project)
end
it { is_expected.to eq(result) }
end
end
context 'when Protected Environments feature is not available in the project' do
let(:enabled) { true }
where(:access_level, :result) do
:guest | false
:reporter | false
:developer | developer_access
:maintainer | true
:admin | true
end
with_them do
before do
environment
update_user_access(access_level, user, project)
end
it { is_expected.to eq(result) }
end
end
context 'when Protected Environments feature is available in the project' do
let(:enabled) { true }
before do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:protected_environments).and_return(true)
end
context 'when environment is protected' do
let(:protected_environment) { create(:protected_environment, name: environment.name, project: project) }
context 'when user does not have access to the environment' do
where(:access_level, :result) do
:guest | false
:reporter | false
:developer | false
:maintainer | false
:admin | true
end
with_them do
before do
protected_environment
update_user_access(access_level, user, project)
end
it { is_expected.to eq(result) }
end
end
context 'when user has access to the environment' do
where(:access_level, :result) do
:guest | false
:reporter | false
:developer | developer_access
:maintainer | true
:admin | true
end
with_them do
before do
protected_environment.deploy_access_levels.create(user: user)
update_user_access(access_level, user, project)
end
it { is_expected.to eq(result) }
end
end
end
context 'when environment is not protected' do
where(:access_level, :result) do
:guest | false
:reporter | false
:developer | developer_access
:maintainer | true
:admin | true
end
with_them do
before do
update_user_access(access_level, user, project)
end
it { is_expected.to eq(result) }
end
end
end
def update_user_access(access_level, user, project)
if access_level == :admin
user.update_attribute(:admin, true)
elsif access_level.present?
project.add_user(user, access_level)
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'restricts access to protected environments' do |developer_access_when_protected, developer_access_when_unprotected|
context 'when build is related to a protected environment' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:environment) { create(:environment, project: project, name: 'production') }
let(:build) { create(:ci_build, :created, pipeline: pipeline, environment: environment.name) }
let(:protected_environment) { create(:protected_environment, name: environment.name, project: project) }
let(:service) { described_class.new(project, user) }
before do
allow(project).to receive(:feature_available?).and_call_original
allow(project).to receive(:feature_available?)
.with(:protected_environments).and_return(true)
project.add_developer(user)
protected_environment
end
context 'when user does not have access to the environment' do
it 'should raise Gitlab::Access::DeniedError' do
expect { service.execute(build) }
.to raise_error Gitlab::Access::AccessDeniedError
end
end
context 'when user has access to the environment' do
before do
protected_environment.deploy_access_levels.create(user: user)
end
it 'enqueues the build' do
build_enqueued = service.execute(build)
expect(build_enqueued).to be_pending
end
end
end
end
......@@ -3,6 +3,8 @@ module Gitlab
module Status
module Build
class Failed < Status::Extended
prepend ::EE::Gitlab::Ci::Status::Build::Failed
REASONS = {
unknown_failure: 'unknown failure',
script_failure: 'script failure',
......@@ -11,7 +13,7 @@ module Gitlab
runner_system_failure: 'runner system failure',
missing_dependency_failure: 'missing dependency failure',
runner_unsupported: 'unsupported runner'
}.freeze
}.merge(EE_REASONS).freeze
def status_tooltip
base_message
......
......@@ -61,6 +61,8 @@ project_tree:
- :merge_access_levels
- :push_access_levels
- :unprotect_access_levels
- protected_environments:
- :deploy_access_levels
- protected_tags:
- :create_access_levels
- :project_feature
......
module Gitlab
module ImportExport
class RelationFactory
prepend ::EE::Gitlab::ImportExport::RelationFactory
OVERRIDES = { snippets: :project_snippets,
pipelines: 'Ci::Pipeline',
stages: 'Ci::Stage',
......@@ -11,7 +13,6 @@ module Gitlab
hooks: 'ProjectHook',
merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
push_access_levels: 'ProtectedBranch::PushAccessLevel',
unprotect_access_levels: 'ProtectedBranch::UnprotectAccessLevel',
create_access_levels: 'ProtectedTag::CreateAccessLevel',
labels: :project_labels,
priorities: :label_priorities,
......@@ -48,7 +49,7 @@ module Gitlab
end
def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project:, excluded_keys: [])
@relation_name = OVERRIDES[relation_sym] || relation_sym
@relation_name = self.class.overrides[relation_sym] || relation_sym
@relation_hash = relation_hash.except('noteable_id')
@members_mapper = members_mapper
@user = user
......@@ -77,6 +78,10 @@ module Gitlab
generate_imported_object
end
def self.overrides
OVERRIDES
end
private
def setup_models
......
......@@ -2867,6 +2867,9 @@ msgstr ""
msgid "Environments|You don't have any environments right now."
msgstr ""
msgid "Environments|protected"
msgstr ""
msgid "Epic"
msgstr ""
......@@ -5722,6 +5725,51 @@ msgstr ""
msgid "Promotions|Upgrade plan"
msgstr ""
msgid "Protected Environments"
msgstr ""
msgid "ProtectedEnvironment|%{environment_name} will be writable for developers. Are you sure?"
msgstr ""
msgid "ProtectedEnvironment|Allowed to deploy"
msgstr ""
msgid "ProtectedEnvironment|Choose who is allowed to deploy"
msgstr ""
msgid "ProtectedEnvironment|Environment"
msgstr ""
msgid "ProtectedEnvironment|Protect"
msgstr ""
msgid "ProtectedEnvironment|Protect Environments in order to restrict who can execute deployments."
msgstr ""
msgid "ProtectedEnvironment|Protect an environment"
msgstr ""
msgid "ProtectedEnvironment|Protected Environment (%{protected_environments_count})"
msgstr ""
msgid "ProtectedEnvironment|Select an environment"
msgstr ""
msgid "ProtectedEnvironment|There are currently no protected environments, protect an environment with the form above."
msgstr ""
msgid "ProtectedEnvironment|Unprotect"
msgstr ""
msgid "ProtectedEnvironment|Your environment can't be unprotected"
msgstr ""
msgid "ProtectedEnvironment|Your environment has been protected."
msgstr ""
msgid "ProtectedEnvironment|Your environment has been unprotected"
msgstr ""
msgid "Protip:"
msgstr ""
......
......@@ -179,6 +179,13 @@ protected_branches:
- merge_access_levels
- push_access_levels
- unprotect_access_levels
protected_environments:
- project
- deploy_access_levels
deploy_access_levels:
- protected_environment
- user
- group
protected_tags:
- project
- create_access_levels
......@@ -266,6 +273,7 @@ project:
- snippets
- hooks
- protected_branches
- protected_environments
- protected_tags
- project_members
- users
......
......@@ -603,3 +603,17 @@ Badge:
- type
ProjectCiCdSetting:
- group_runners_enabled
ProtectedEnvironment:
- id
- project_id
- name
- created_at
- updated_at
ProtectedEnvironment::DeployAccessLevel:
- id
- protected_environment_id
- access_level
- created_at
- updated_at
- user_id
- group_id
# frozen_string_literal: true
require 'spec_helper'
describe Ci::EnqueueBuildService, '#execute' do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:ci_build) { create(:ci_build, :created) }
subject { described_class.new(project, user).execute(ci_build) }
it 'enqueues the build' do
subject
expect(ci_build.pending?).to be_truthy
end
end
......@@ -9,7 +9,7 @@ module ConfigurationHelper
end
def relation_class_for_name(relation_name)
relation_name = Gitlab::ImportExport::RelationFactory::OVERRIDES[relation_name.to_sym] || relation_name
relation_name = Gitlab::ImportExport::RelationFactory.overrides[relation_name.to_sym] || relation_name
Gitlab::ImportExport::RelationFactory.relation_class(relation_name)
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