Skip to content
Snippets Groups Projects
Commit 1dd0d56f authored by Mayra Cabrera's avatar Mayra Cabrera Committed by Kamil Trzciński
Browse files

Protected Environments - Backend

parent a63e886d
No related branches found
No related tags found
No related merge requests found
Showing
with 404 additions and 25 deletions
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -7,6 +7,8 @@ class CommitStatus < ActiveRecord::Base
include Presentable
include EnumWithNil
 
prepend ::EE::CommitStatus
self.table_name = 'ci_builds'
 
belongs_to :user
Loading
Loading
@@ -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.
Loading
Loading
Loading
Loading
@@ -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)
 
Loading
Loading
# frozen_string_literal: true
 
class EnvironmentPolicy < BasePolicy
prepend EE::EnvironmentPolicy
delegate { @subject.project }
 
condition(:stop_with_deployment_allowed) do
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
# 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,
Loading
Loading
@@ -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
 
Loading
Loading
Loading
Loading
@@ -2,7 +2,6 @@
 
class EnvironmentEntity < Grape::Entity
include RequestAwareEntity
prepend ::EE::EnvironmentEntity
 
expose :id
Loading
Loading
@@ -26,9 +25,8 @@ 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) &&
terminal_project_environment_path(environment.project, environment)
expose :terminal_path, if: ->(*) { environment.has_terminals? && can_access_terminal? } do |environment|
terminal_project_environment_path(environment.project, environment)
end
 
expose :folder_path do |environment|
Loading
Loading
@@ -52,4 +50,8 @@ def current_user
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
Loading
Loading
@@ -37,7 +37,7 @@ def process_stage(index)
 
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
Loading
Loading
@@ -93,5 +93,9 @@ def update_retried
.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
Loading
Loading
@@ -285,6 +285,14 @@
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
Loading
Loading
Loading
Loading
@@ -920,6 +920,7 @@
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
 
Loading
Loading
@@ -2283,6 +2284,29 @@
 
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
Loading
Loading
@@ -3089,6 +3113,10 @@
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"
Loading
Loading
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', () => {
Loading
Loading
@@ -18,4 +20,10 @@ document.addEventListener('DOMContentLoaded', () => {
},
});
}
// eslint-disable-next-line no-new
new ProtectedEnvironmentCreate();
// eslint-disable-next-line no-new
new ProtectedEnvironmentEditList();
});
Loading
Loading
@@ -49,23 +49,25 @@ export default class AccessDropdown {
e.preventDefault();
 
if ($el.is('.is-active')) {
if (item.id === this.noOneObj.id) {
// remove all others selected items
this.accessLevelsData.forEach(level => {
if (level.id !== item.id) {
this.removeSelectedItem(level);
if (this.noOneObj) {
if (item.id === this.noOneObj.id) {
// remove all others selected items
this.accessLevelsData.forEach(level => {
if (level.id !== item.id) {
this.removeSelectedItem(level);
}
});
// remove selected item visually
this.$wrap.find(`.item-${item.type}`).removeClass('is-active');
} else {
const $noOne = this.$wrap.find(
`.is-active.item-${item.type}[data-role-id="${this.noOneObj.id}"]`,
);
if ($noOne.length) {
$noOne.removeClass('is-active');
this.removeSelectedItem(this.noOneObj);
}
});
// remove selected item visually
this.$wrap.find(`.item-${item.type}`).removeClass('is-active');
} else {
const $noOne = this.$wrap.find(
`.is-active.item-${item.type}[data-role-id="${this.noOneObj.id}"]`,
);
if ($noOne.length) {
$noOne.removeClass('is-active');
this.removeSelectedItem(this.noOneObj);
}
}
 
Loading
Loading
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),
});
});
}
}
Loading
Loading
@@ -5,6 +5,10 @@
}
}
 
.protected-environments-list {
@extend .protected-branches-list;
}
.project-mirror-settings {
.fingerprint-verified {
color: $green-500;
Loading
Loading
Loading
Loading
@@ -6,6 +6,7 @@ module EnvironmentsController
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
Loading
Loading
@@ -31,6 +32,10 @@ def environment_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
Loading
Loading
@@ -14,7 +21,24 @@ def show
 
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
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment