Commit 7c6633b4 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'bvl-license-check-protected-refs' into 'master'

Add a license check for permissions to users/groups on protected refs

Closes #2574

See merge request !2320
parents 677bb1e3 fa79a74e
...@@ -42,7 +42,6 @@ import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; ...@@ -42,7 +42,6 @@ import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
import Landing from './landing'; import Landing from './landing';
import BlobForkSuggestion from './blob/blob_fork_suggestion'; import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout'; import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
import ShortcutsWiki from './shortcuts_wiki'; import ShortcutsWiki from './shortcuts_wiki';
import Pipelines from './pipelines'; import Pipelines from './pipelines';
import BlobViewer from './blob/viewer/index'; import BlobViewer from './blob/viewer/index';
...@@ -415,9 +414,6 @@ import AuditLogs from './audit_logs'; ...@@ -415,9 +414,6 @@ import AuditLogs from './audit_logs';
new gl.ProtectedBranchCreate(); new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList(); new gl.ProtectedBranchEditList();
new UsersSelect(); new UsersSelect();
// Initialize Protected Tag Settings
new ProtectedTagCreate();
new ProtectedTagEditList();
// Initialize expandable settings panels // Initialize expandable settings panels
initSettingsPanels(); initSettingsPanels();
break; break;
......
/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, guard-for-in, no-restricted-syntax, max-len */
/* global ProtectedBranchDropdown */
/* global Flash */
(global => {
global.gl = global.gl || {};
const ACCESS_LEVELS = {
MERGE: 'merge_access_levels',
PUSH: 'push_access_levels',
};
const LEVEL_TYPES = {
ROLE: 'role',
USER: 'user',
GROUP: 'group'
};
gl.ProtectedBranchCreate = class {
constructor() {
this.$wrap = this.$form = $('#new_protected_branch');
this.buildDropdowns();
this.$branchInput = this.$wrap.find('input[name="protected_branch[name]"]');
this.bindEvents();
}
bindEvents() {
this.$form.on('submit', this.onFormSubmit.bind(this));
}
buildDropdowns() {
const $allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
const $allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push');
// Cache callback
this.onSelectCallback = this.onSelect.bind(this);
// Allowed to Merge dropdown
this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new gl.ProtectedBranchAccessDropdown({
$dropdown: $allowedToMergeDropdown,
accessLevelsData: gon.merge_access_levels,
onSelect: this.onSelectCallback,
accessLevel: ACCESS_LEVELS.MERGE
});
// Allowed to Push dropdown
this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new gl.ProtectedBranchAccessDropdown({
$dropdown: $allowedToPushDropdown,
accessLevelsData: gon.push_access_levels,
onSelect: this.onSelectCallback,
accessLevel: ACCESS_LEVELS.PUSH
});
// Protected branch dropdown
new window.ProtectedBranchDropdown({
$dropdown: this.$wrap.find('.js-protected-branch-select'),
onSelect: this.onSelectCallback
});
}
// Enable submit button after selecting an option
onSelect() {
const $allowedToMerge = this[`${ACCESS_LEVELS.MERGE}_dropdown`].getSelectedItems();
const $allowedToPush = this[`${ACCESS_LEVELS.PUSH}_dropdown`].getSelectedItems();
const toggle = !(this.$wrap.find('input[name="protected_branch[name]"]').val() && $allowedToMerge.length && $allowedToPush.length);
this.$form.find('input[type="submit"]').attr('disabled', toggle);
}
getFormData() {
const formData = {
authenticity_token: this.$form.find('input[name="authenticity_token"]').val(),
protected_branch: {
name: this.$wrap.find('input[name="protected_branch[name]"]').val(),
}
};
for (const ACCESS_LEVEL in ACCESS_LEVELS) {
const selectedItems = this[`${ACCESS_LEVELS[ACCESS_LEVEL]}_dropdown`].getSelectedItems();
const levelAttributes = [];
for (let i = 0; i < selectedItems.length; i += 1) {
const current = selectedItems[i];
if (current.type === LEVEL_TYPES.USER) {
levelAttributes.push({
user_id: selectedItems[i].user_id
});
} else if (current.type === LEVEL_TYPES.ROLE) {
levelAttributes.push({
access_level: selectedItems[i].access_level
});
} else if (current.type === LEVEL_TYPES.GROUP) {
levelAttributes.push({
group_id: selectedItems[i].group_id
});
}
}
formData.protected_branch[`${ACCESS_LEVELS[ACCESS_LEVEL]}_attributes`] = levelAttributes;
}
return formData;
}
onFormSubmit(e) {
e.preventDefault();
$.ajax({
url: this.$form.attr('action'),
method: this.$form.attr('method'),
data: this.getFormData()
})
.success(() => {
location.reload();
})
.fail(() => {
new Flash('Failed to protect the branch');
});
}
};
})(window);
/* eslint-disable comma-dangle, no-unused-vars */
class ProtectedBranchDropdown {
constructor(options) {
this.onSelect = options.onSelect;
this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent();
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
this.$protectedBranch = this.$dropdownContainer.find('.js-create-new-protected-branch');
this.buildDropdown();
this.bindEvents();
// Hide footer
this.$dropdownFooter.addClass('hidden');
}
buildDropdown() {
this.$dropdown.glDropdown({
data: this.getProtectedBranches.bind(this),
filterable: true,
remote: false,
search: {
fields: ['title']
},
selectable: true,
toggleLabel(selected) {
return (selected && 'id' in selected) ? selected.title : 'Protected Branch';
},
fieldName: 'protected_branch[name]',
text(protectedBranch) {
return _.escape(protectedBranch.title);
},
id(protectedBranch) {
return _.escape(protectedBranch.id);
},
onFilter: this.toggleCreateNewButton.bind(this),
clicked: (options) => {
const { $el, e } = options;
e.preventDefault();
this.onSelect();
}
});
}
bindEvents() {
this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this));
}
onClickCreateWildcard(e) {
e.preventDefault();
// Refresh the dropdown's data, which ends up calling `getProtectedBranches`
this.$dropdown.data('glDropdown').remote.execute();
this.$dropdown.data('glDropdown').selectRowAtIndex();
}
getProtectedBranches(term, callback) {
if (this.selectedBranch) {
callback(gon.open_branches.concat(this.selectedBranch));
} else {
callback(gon.open_branches);
}
}
toggleCreateNewButton(branchName) {
this.selectedBranch = {
title: branchName,
id: branchName,
text: branchName
};
if (branchName) {
this.$dropdownContainer
.find('.js-create-new-protected-branch code')
.text(branchName);
}
this.$dropdownFooter.toggleClass('hidden', !branchName);
}
}
window.ProtectedBranchDropdown = ProtectedBranchDropdown;
/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, dot-notation, no-unused-vars, no-restricted-syntax, guard-for-in, max-len */
/* global Flash */
(global => {
global.gl = global.gl || {};
const ACCESS_LEVELS = {
MERGE: 'merge_access_levels',
PUSH: 'push_access_levels',
};
const LEVEL_TYPES = {
ROLE: 'role',
USER: 'user',
GROUP: 'group'
};
gl.ProtectedBranchEdit = class {
constructor(options) {
this.$wraps = {};
this.hasChanges = false;
this.$wrap = options.$wrap;
this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push');
this.$wraps[ACCESS_LEVELS.MERGE] = this.$allowedToMergeDropdown.closest(`.${ACCESS_LEVELS.MERGE}-container`);
this.$wraps[ACCESS_LEVELS.PUSH] = this.$allowedToPushDropdown.closest(`.${ACCESS_LEVELS.PUSH}-container`);
this.buildDropdowns();
}
buildDropdowns() {
// Allowed to merge dropdown
this['merge_access_levels_dropdown'] = new gl.ProtectedBranchAccessDropdown({
accessLevel: ACCESS_LEVELS.MERGE,
accessLevelsData: gon.merge_access_levels,
$dropdown: this.$allowedToMergeDropdown,
onSelect: this.onSelectOption.bind(this),
onHide: this.onDropdownHide.bind(this)
});
// Allowed to push dropdown
this['push_access_levels_dropdown'] = new gl.ProtectedBranchAccessDropdown({
accessLevel: ACCESS_LEVELS.PUSH,
accessLevelsData: gon.push_access_levels,
$dropdown: this.$allowedToPushDropdown,
onSelect: this.onSelectOption.bind(this),
onHide: this.onDropdownHide.bind(this)
});
}
onSelectOption(item, $el, dropdownInstance) {
this.hasChanges = true;
}
onDropdownHide() {
if (!this.hasChanges) return;
this.hasChanges = true;
this.updatePermissions();
}
updatePermissions() {
const formData = {};
for (const ACCESS_LEVEL in ACCESS_LEVELS) {
const accessLevelName = ACCESS_LEVELS[ACCESS_LEVEL];
formData[`${accessLevelName}_attributes`] = this[`${accessLevelName}_dropdown`].getInputData(accessLevelName);
}
return $.ajax({
type: 'POST',
url: this.$wrap.data('url'),
dataType: 'json',
data: {
_method: 'PATCH',
protected_branch: formData
},
success: (response) => {
this.hasChanges = false;
for (const ACCESS_LEVEL in ACCESS_LEVELS) {
const accessLevelName = ACCESS_LEVELS[ACCESS_LEVEL];
// The data coming from server will be the new persisted *state* for each dropdown
this.setSelectedItemsToDropdown(response[accessLevelName], `${accessLevelName}_dropdown`);
}
},
error() {
$.scrollTo(0);
new Flash('Failed to update branch!');
}
}).always(() => {
this.$allowedToMergeDropdown.enable();
this.$allowedToPushDropdown.enable();
});
}
setSelectedItemsToDropdown(items = [], dropdownName) {
const itemsToAdd = [];
for (let i = 0; i < items.length; i += 1) {
let itemToAdd;
const currentItem = items[i];
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 });
itemToAdd = {
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) {
itemToAdd = {
id: currentItem.id,
group_id: currentItem.group_id,
type: LEVEL_TYPES.GROUP,
persisted: true
};
} else {
itemToAdd = {
id: currentItem.id,
access_level: currentItem.access_level,
type: LEVEL_TYPES.ROLE,
persisted: true
};
}
itemsToAdd.push(itemToAdd);
}
this[dropdownName].setSelectedItems(itemsToAdd);
}
};
})(window);
/* eslint-disable arrow-parens, no-param-reassign, no-new, comma-dangle */
(global => {
global.gl = global.gl || {};
gl.ProtectedBranchEditList = class {
constructor() {
this.$wrap = $('.protected-branches-list');
// Build edit forms
this.$wrap.find('.js-protected-branch-edit-form').each((i, el) => {
new gl.ProtectedBranchEdit({
$wrap: $(el)
});
});
}
};
})(window);
import './protected_branch_access_dropdown';
import './protected_branch_create';
import './protected_branch_dropdown';
import './protected_branch_edit';
import './protected_branch_edit_list';
/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, guard-for-in, no-restricted-syntax, max-len */ /* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */
/* global ProtectedBranchDropdown */ /* global ProtectedBranchDropdown */
/* global Flash */
(global => { (global => {
global.gl = global.gl || {}; global.gl = global.gl || {};
const ACCESS_LEVELS = {
MERGE: 'merge_access_levels',
PUSH: 'push_access_levels',
};
const LEVEL_TYPES = {
ROLE: 'role',
USER: 'user',
GROUP: 'group'
};
gl.ProtectedBranchCreate = class { gl.ProtectedBranchCreate = class {
constructor() { constructor() {
this.$wrap = this.$form = $('#new_protected_branch'); this.$wrap = this.$form = $('#new_protected_branch');
this.buildDropdowns(); this.buildDropdowns();
this.$branchInput = this.$wrap.find('input[name="protected_branch[name]"]');
this.bindEvents();
}
bindEvents() {
this.$form.on('submit', this.onFormSubmit.bind(this));
} }
buildDropdowns() { buildDropdowns() {
...@@ -36,87 +18,38 @@ ...@@ -36,87 +18,38 @@
this.onSelectCallback = this.onSelect.bind(this); this.onSelectCallback = this.onSelect.bind(this);
// Allowed to Merge dropdown // Allowed to Merge dropdown
this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new gl.ProtectedBranchAccessDropdown({ new gl.ProtectedBranchAccessDropdown({
$dropdown: $allowedToMergeDropdown, $dropdown: $allowedToMergeDropdown,
accessLevelsData: gon.merge_access_levels, data: gon.merge_access_levels,
onSelect: this.onSelectCallback, onSelect: this.onSelectCallback
accessLevel: ACCESS_LEVELS.MERGE
}); });
// Allowed to Push dropdown // Allowed to Push dropdown
this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new gl.ProtectedBranchAccessDropdown({ new gl.ProtectedBranchAccessDropdown({
$dropdown: $allowedToPushDropdown, $dropdown: $allowedToPushDropdown,
accessLevelsData: gon.push_access_levels, data: gon.push_access_levels,
onSelect: this.onSelectCallback, onSelect: this.onSelectCallback
accessLevel: ACCESS_LEVELS.PUSH
}); });
// Select default
$allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0);
$allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0);
// Protected branch dropdown // Protected branch dropdown
new window.ProtectedBranchDropdown({ new ProtectedBranchDropdown({
$dropdown: this.$wrap.find('.js-protected-branch-select'), $dropdown: this.$wrap.find('.js-protected-branch-select'),
onSelect: this.onSelectCallback onSelect: this.onSelectCallback
}); });
} }
// Enable submit button after selecting an option // This will run after clicked callback
onSelect() { onSelect() {
const $allowedToMerge = this[`${ACCESS_LEVELS.MERGE}_dropdown`].getSelectedItems(); // Enable submit button
const $allowedToPush = this[`${ACCESS_LEVELS.PUSH}_dropdown`].getSelectedItems(); const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]');
const toggle = !(this.$wrap.find('input[name="protected_branch[name]"]').val() && $allowedToMerge.length && $allowedToPush.length); const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]');
this.$form.find('input[type="submit"]').attr('disabled', toggle);
}
getFormData() {
const formData = {
authenticity_token: this.$form.find('input[name="authenticity_token"]').val(),
protected_branch: {
name: this.$wrap.find('input[name="protected_branch[name]"]').val(),
}
};
for (const ACCESS_LEVEL in ACCESS_LEVELS) {
const selectedItems = this[`${ACCESS_LEVELS[ACCESS_LEVEL]}_dropdown`].getSelectedItems();
const levelAttributes = [];
for (let i = 0; i < selectedItems.length; i += 1) {
const current = selectedItems[i];
if (current.type === LEVEL_TYPES.USER) {
levelAttributes.push({
user_id: selectedItems[i].user_id
});
} else if (current.type === LEVEL_TYPES.ROLE) {
levelAttributes.push({
access_level: selectedItems[i].access_level
});
} else if (current.type === LEVEL_TYPES.GROUP) {
levelAttributes.push({
group_id: selectedItems[i].group_id
});
}
}
formData.protected_branch[`${ACCESS_LEVELS[ACCESS_LEVEL]}_attributes`] = levelAttributes; this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length));
}
return formData;
}
onFormSubmit(e) {
e.preventDefault();
$.ajax({
url: this.$form.attr('action'),
method: this.$form.attr('method'),
data: this.getFormData()
})
.success(() => {
location.reload();
})
.fail(() => {
new Flash('Failed to protect the branch');
});
} }
}; };
})(window); })(window);
/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, dot-notation, no-unused-vars, no-restricted-syntax, guard-for-in, max-len */ /* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */
/* global Flash */ /* global Flash */
(global => { (global => {
global.gl = global.gl || {}; global.gl = global.gl || {};
const ACCESS_LEVELS = {
MERGE: 'merge_access_levels',
PUSH: 'push_access_levels',
};
const LEVEL_TYPES = {
ROLE: 'role',
USER: 'user',
GROUP: 'group'
};
gl.ProtectedBranchEdit = class { gl.ProtectedBranchEdit = class {
constructor(options) { constructor(options) {
this.$wraps = {};
this.hasChanges = false;
this.$wrap = options.$wrap; this.$wrap = options.$wrap;
this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push');
this.$wraps[ACCESS_LEVELS.MERGE] = this.$allowedToMergeDropdown.closest(`.${ACCESS_LEVELS.MERGE}-container`);
this.$wraps[ACCESS_LEVELS.PUSH] = this.$allowedToPushDropdown.closest(`.${ACCESS_LEVELS.PUSH}-container`);
this.buildDropdowns(); this.buildDropdowns();
} }
buildDropdowns() { buildDropdowns() {
// Allowed to merge dropdown // Allowed to merge dropdown
this['merge_access_levels_dropdown'] = new gl.ProtectedBranchAccessDropdown({ new gl.ProtectedBranchAccessDropdown({
accessLevel: ACCESS_LEVELS.MERGE,
accessLevelsData: gon.merge_access_levels,
$dropdown: this.$allowedToMergeDropdown, $dropdown: this.$allowedToMergeDropdown,
onSelect: this.onSelectOption.bind(this), data: gon.merge_access_levels,
onHide: this.onDropdownHide.bind(this) onSelect: this.onSelect.bind(this)
}); });
// Allowed to push dropdown // Allowed to push dropdown
this['push_access_levels_dropdown'] = new gl.ProtectedBranchAccessDropdown({ new gl.ProtectedBranchAccessDropdown({
accessLevel: ACCESS_LEVELS.PUSH,
accessLevelsData: gon.push_access_levels,
$dropdown: this.$allowedToPushDropdown, $dropdown: this.$allowedToPushDropdown,
onSelect: this.onSelectOption.bind(this), data: gon.push_access_levels,
onHide: this.onDropdownHide.bind(this) onSelect: this.onSelect.bind(this)
}); });
} }
onSelectOption(item, $el, dropdownInstance) { onSelect() {
this.hasChanges = true; const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`);
} const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`);
onDropdownHide() {
if (!this.hasChanges) return;
this.hasChanges = true;
this.updatePermissions();
}
updatePermissions() {
const formData = {};
for (const ACCESS_LEVEL in ACCESS_LEVELS) { // Do not update if one dropdown has not selected any option
const accessLevelName = ACCESS_LEVELS[ACCESS_LEVEL]; if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
formData[`${accessLevelName}_attributes`] = this[`${accessLevelName}_dropdown`].getInputData(accessLevelName); this.$allowedToMergeDropdown.disable();
} this.$allowedToPushDropdown.disable();
return $.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: this.$wrap.data('url'), url: this.$wrap.data('url'),
dataType: 'json', dataType: 'json',
data: { data: {
_method: 'PATCH', _method: 'PATCH',
protected_branch: formData protected_branch: {
}, merge_access_levels_attributes: [{
success: (response) => { id: this.$allowedToMergeDropdown.data('access-level-id'),
this.hasChanges = false; access_level: $allowedToMergeInput.val()
}],
for (const ACCESS_LEVEL in ACCESS_LEVELS) { push_access_levels_attributes: [{
const accessLevelName = ACCESS_LEVELS[ACCESS_LEVEL]; id: this.$allowedToPushDropdown.data('access-level-id'),
access_level: $allowedToPushInput.val()
// The data coming from server will be the new persisted *state* for each dropdown }]
this.setSelectedItemsToDropdown(response[accessLevelName], `${accessLevelName}_dropdown`);
} }
}, },
error() { error() {
...@@ -97,49 +65,5 @@ ...@@ -97,49 +65,5 @@
this.$allowedToPushDropdown.enable(); this.$allowedToPushDropdown.enable();
}); });
} }
setSelectedItemsToDropdown(items = [], dropdownName) {
const itemsToAdd = [];
for (let i = 0; i < items.length; i += 1) {
let itemToAdd;
const currentItem = items[i];
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 });
itemToAdd = {
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) {
itemToAdd = {
id: currentItem.id,
group_id: currentItem.group_id,
type: LEVEL_TYPES.GROUP,
persisted: true
};
} else {
itemToAdd = {
id: currentItem.id,
access_level: currentItem.access_level,
type: LEVEL_TYPES.ROLE,
persisted: true
};
}
itemsToAdd.push(itemToAdd);
}
this[dropdownName].setSelectedItems(itemsToAdd);
}
}; };
})(window); })(window);
/* eslint-disable no-unused-vars */
import ProtectedTagCreate from './protected_tag_create';
import ProtectedTagEditList from './protected_tag_edit_list';
$(() => {
const protectedtTagCreate = new ProtectedTagCreate();
const protectedtTagEditList = new ProtectedTagEditList();
});
/* global Flash */
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
import ProtectedTagDropdown from './protected_tag_dropdown';
export default class ProtectedTagCreate {
constructor() {
this.$form = $('.js-new-protected-tag');
this.buildDropdowns();
this.$branchTag = this.$form.find('input[name="protected_tag[name]"]');
this.bindEvents();
}
bindEvents() {
this.$form.on('submit', this.onFormSubmit.bind(this));
}
buildDropdowns() {
const $allowedToCreateDropdown = this.$form.find('.js-allowed-to-create');
// Cache callback
this.onSelectCallback = this.onSelect.bind(this);
// Allowed to Create dropdown
this[`${ACCESS_LEVELS.CREATE}_dropdown`] = new ProtectedTagAccessDropdown({
$dropdown: $allowedToCreateDropdown,
accessLevelsData: gon.create_access_levels,
onSelect: this.onSelectCallback,
accessLevel: ACCESS_LEVELS.CREATE,
});
// Protected tag dropdown
this.protectedTagDropdown = new ProtectedTagDropdown({
$dropdown: this.$form.find('.js-protected-tag-select'),
onSelect: this.onSelectCallback,
});
}
// Enable submit button after selecting an option
onSelect() {
const $allowedToCreate = this[`${ACCESS_LEVELS.CREATE}_dropdown`].getSelectedItems();
const toggle = !(this.$form.find('input[name="protected_tag[name]"]').val() && $allowedToCreate.length);
this.$form.find('input[type="submit"]').attr('disabled', toggle);
}
getFormData() {
const formData = {
authenticity_token: this.$form.find('input[name="authenticity_token"]').val(),
protected_tag: {
name: this.$form.find('input[name="protected_tag[name]"]').val(),
},
};
Object.keys(ACCESS_LEVELS).forEach((level) => {
const accessLevel = ACCESS_LEVELS[level];
const selectedItems = this[`${ACCESS_LEVELS.CREATE}_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_tag[`${accessLevel}_attributes`] = levelAttributes;
});
return formData;
}
onFormSubmit(e) {
e.preventDefault();
$.ajax({
url: this.$form.attr('action'),
method: this.$form.attr('method'),
data: this.getFormData(),
})
.success(() => {
location.reload();
})
.fail(() => new Flash('Failed to protect the tag'));
}
}
export default class ProtectedTagDropdown {
/**
* @param {Object} options containing
* `$dropdown` target element
* `onSelect` event callback
* $dropdown must be an element created using `dropdown_tag()` rails helper
*/
constructor(options) {
this.onSelect = options.onSelect;
this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent();
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
this.$protectedTag = this.$dropdownContainer.find('.js-create-new-protected-tag');
this.buildDropdown();
this.bindEvents();
// Hide footer
this.toggleFooter(true);
}
buildDropdown() {
this.$dropdown.glDropdown({
data: this.getProtectedTags.bind(this),
filterable: true,
remote: false,
search: {
fields: ['title'],
},
selectable: true,
toggleLabel(selected) {
return (selected && 'id' in selected) ? selected.title : 'Protected Tag';
},
fieldName: 'protected_tag[name]',
text(protectedTag) {
return _.escape(protectedTag.title);
},
id(protectedTag) {
return _.escape(protectedTag.id);
},
onFilter: this.toggleCreateNewButton.bind(this),
clicked: (options) => {
options.e.preventDefault();
this.onSelect();
},
});
}
bindEvents() {
this.$protectedTag.on('click', this.onClickCreateWildcard.bind(this));
}
onClickCreateWildcard(e) {
this.$dropdown.data('glDropdown').remote.execute();
this.$dropdown.data('glDropdown').selectRowAtIndex();
e.preventDefault();
}
getProtectedTags(term, callback) {
if (this.selectedTag) {
callback(gon.open_tags.concat(this.selectedTag));
} else {
callback(gon.open_tags);
}
}
toggleCreateNewButton(tagName) {
if (tagName) {
this.selectedTag = {
title: tagName,
id: tagName,
text: tagName,
};
this.$dropdownContainer
.find('.js-create-new-protected-tag code')
.text(tagName);
}
this.toggleFooter(!tagName);
}
toggleFooter(toggleState) {
this.$dropdownFooter.toggleClass('hidden', toggleState);
}
}
/* eslint-disable no-new */
/* global Flash */
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
export default class ProtectedTagEdit {
constructor(options) {
this.hasChanges = false;
this.$wrap = options.$wrap;
this.$allowedToCreateDropdownButton = this.$wrap.find('.js-allowed-to-create');
this.$allowedToCreateDropdownContainer = this.$allowedToCreateDropdownButton.closest('.create_access_levels-container');
this.buildDropdowns();
}
buildDropdowns() {
// Allowed to create dropdown
this[`${ACCESS_LEVELS.CREATE}_dropdown`] = new ProtectedTagAccessDropdown({
accessLevel: ACCESS_LEVELS.CREATE,
accessLevelsData: gon.create_access_levels,
$dropdown: this.$allowedToCreateDropdownButton,
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) => {
/* eslint-disable no-param-reassign */
const accessLevelName = ACCESS_LEVELS[level];
const inputData = this[`${accessLevelName}_dropdown`].getInputData(accessLevelName);
acc[`${accessLevelName}_attributes`] = inputData;
return acc;
}, {});
return $.ajax({
type: 'POST',
url: this.$wrap.data('url'),
dataType: 'json',
data: {
_method: 'PATCH',
protected_tag: formData,
},
success: (response) => {
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(response[accessLevelName], `${accessLevelName}_dropdown`);
});
},
error() {
$.scrollTo(0);
new Flash('Failed to update tag!');
},
}).always(() => {
this.$allowedToCreateDropdownButton.enable();
});
}
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 ProtectedTagEdit from './protected_tag_edit';
export default class ProtectedTagEditList {
constructor() {
this.$wrap = $('.protected-tags-list');
this.initEditForm();
}
initEditForm() {
this.$wrap.find('.js-protected-tag-edit-form').each((i, el) => {
new ProtectedTagEdit({
$wrap: $(el),
});
});
}
}
export { default as ProtectedTagCreate } from './protected_tag_create'; /* eslint-disable no-unused-vars */
export { default as ProtectedTagEditList } from './protected_tag_edit_list';
import ProtectedTagCreate from './protected_tag_create';
import ProtectedTagEditList from './protected_tag_edit_list';
$(() => {
const protectedtTagCreate = new ProtectedTagCreate();
const protectedtTagEditList = new ProtectedTagEditList();
});
/* global Flash */
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown'; import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
import ProtectedTagDropdown from './protected_tag_dropdown'; import ProtectedTagDropdown from './protected_tag_dropdown';
...@@ -8,12 +5,6 @@ export default class ProtectedTagCreate { ...@@ -8,12 +5,6 @@ export default class ProtectedTagCreate {
constructor() { constructor() {
this.$form = $('.js-new-protected-tag'); this.$form = $('.js-new-protected-tag');
this.buildDropdowns(); this.buildDropdowns();
this.$branchTag = this.$form.find('input[name="protected_tag[name]"]');
this.bindEvents();
}
bindEvents() {
this.$form.on('submit', this.onFormSubmit.bind(this));
} }
buildDropdowns() { buildDropdowns() {
...@@ -23,13 +14,15 @@ export default class ProtectedTagCreate { ...@@ -23,13 +14,15 @@ export default class ProtectedTagCreate {
this.onSelectCallback = this.onSelect.bind(this); this.onSelectCallback = this.onSelect.bind(this);
// Allowed to Create dropdown // Allowed to Create dropdown
this[`${ACCESS_LEVELS.CREATE}_dropdown`] = new ProtectedTagAccessDropdown({ this.protectedTagAccessDropdown = new ProtectedTagAccessDropdown({
$dropdown: $allowedToCreateDropdown, $dropdown: $allowedToCreateDropdown,
accessLevelsData: gon.create_access_levels, data: gon.create_access_levels,
onSelect: this.onSelectCallback, onSelect: this.onSelectCallback,
accessLevel: ACCESS_LEVELS.CREATE,
}); });
// Select default
$allowedToCreateDropdown.data('glDropdown').selectRowAtIndex(0);
// Protected tag dropdown // Protected tag dropdown
this.protectedTagDropdown = new ProtectedTagDropdown({ this.protectedTagDropdown = new ProtectedTagDropdown({
$dropdown: this.$form.find('.js-protected-tag-select'), $dropdown: this.$form.find('.js-protected-tag-select'),
...@@ -37,60 +30,12 @@ export default class ProtectedTagCreate { ...@@ -37,60 +30,12 @@ export default class ProtectedTagCreate {
}); });
} }
// Enable submit button after selecting an option // This will run after clicked callback
onSelect() { onSelect() {
const $allowedToCreate = this[`${ACCESS_LEVELS.CREATE}_dropdown`].getSelectedItems(); // Enable submit button
const toggle = !(this.$form.find('input[name="protected_tag[name]"]').val() && $allowedToCreate.length); const $tagInput = this.$form.find('input[name="protected_tag[name]"]');
const $allowedToCreateInput = this.$form.find('#create_access_levels_attributes');
this.$form.find('input[type="submit"]').attr('disabled', toggle);
}
getFormData() {
const formData = {
authenticity_token: this.$form.find('input[name="authenticity_token"]').val(),
protected_tag: {
name: this.$form.find('input[name="protected_tag[name]"]').val(),
},
};
Object.keys(ACCESS_LEVELS).forEach((level) => {
const accessLevel = ACCESS_LEVELS[level];
const selectedItems = this[`${ACCESS_LEVELS.CREATE}_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_tag[`${accessLevel}_attributes`] = levelAttributes;
});
return formData;
}
onFormSubmit(e) {
e.preventDefault();
$.ajax({ this.$form.find('input[type="submit"]').attr('disabled', !($tagInput.val() && $allowedToCreateInput.length));
url: this.$form.attr('action'),
method: this.$form.attr('method'),
data: this.getFormData(),
})
.success(() => {
location.reload();
})
.fail(() => new Flash('Failed to protect the tag'));
} }
} }
/* eslint-disable no-new */ /* eslint-disable no-new */
/* global Flash */ /* global Flash */
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown'; import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
export default class ProtectedTagEdit { export default class ProtectedTagEdit {
constructor(options) { constructor(options) {
this.hasChanges = false;
this.$wrap = options.$wrap; this.$wrap = options.$wrap;
this.$allowedToCreateDropdownButton = this.$wrap.find('.js-allowed-to-create'); this.$allowedToCreateDropdownButton = this.$wrap.find('.js-allowed-to-create');
this.onSelectCallback = this.onSelect.bind(this);
this.$allowedToCreateDropdownContainer = this.$allowedToCreateDropdownButton.closest('.create_access_levels-container');
this.buildDropdowns(); this.buildDropdowns();
} }
buildDropdowns() { buildDropdowns() {
// Allowed to create dropdown // Allowed to create dropdown
this[`${ACCESS_LEVELS.CREATE}_dropdown`] = new ProtectedTagAccessDropdown({ this.protectedTagAccessDropdown = new ProtectedTagAccessDropdown({
accessLevel: ACCESS_LEVELS.CREATE,
accessLevelsData: gon.create_access_levels,
$dropdown: this.$allowedToCreateDropdownButton, $dropdown: this.$allowedToCreateDropdownButton,
onSelect: this.onSelectOption.bind(this), data: gon.create_access_levels,
onHide: this.onDropdownHide.bind(this), onSelect: this.onSelectCallback,
}); });
} }
onSelectOption() { onSelect() {
this.hasChanges = true; const $allowedToCreateInput = this.$wrap.find(`input[name="${this.$allowedToCreateDropdownButton.data('fieldName')}"]`);
}
onDropdownHide() {
if (!this.hasChanges) {
return;
}
this.hasChanges = true; // Do not update if one dropdown has not selected any option
this.updatePermissions(); if (!$allowedToCreateInput.length) return;
}
updatePermissions() { this.$allowedToCreateDropdownButton.disable();
const formData = Object.keys(ACCESS_LEVELS).reduce((acc, level) => {
/* eslint-disable no-param-reassign */
const accessLevelName = ACCESS_LEVELS[level];
const inputData = this[`${accessLevelName}_dropdown`].getInputData(accessLevelName);
acc[`${accessLevelName}_attributes`] = inputData;
return acc; $.ajax({
}, {});
return $.ajax({
type: 'POST', type: 'POST',
url: this.$wrap.data('url'), url: this.$wrap.data('url'),
dataType: 'json', dataType: 'json',
data: { data: {
_method: 'PATCH', _method: 'PATCH',
protected_tag: formData, protected_tag: {
}, create_access_levels_attributes: [{
success: (response) => { id: this.$allowedToCreateDropdownButton.data('access-level-id'),
this.hasChanges = false; access_level: $allowedToCreateInput.val(),
}],
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(response[accessLevelName], `${accessLevelName}_dropdown`);
});
}, },
error() { error() {
$.scrollTo(0); new Flash('Failed to update tag!', null, $('.js-protected-tags-list'));
new Flash('Failed to update tag!');
}, },
}).always(() => { }).always(() => {
this.$allowedToCreateDropdownButton.enable(); this.$allowedToCreateDropdownButton.enable();
}); });
} }
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);
}
} }
...@@ -755,6 +755,10 @@ a.allowed-to-push { ...@@ -755,6 +755,10 @@ a.allowed-to-push {
.table-bordered { .table-bordered {
border-radius: 1px; border-radius: 1px;
td {
vertical-align: top;
}
th:not(:last-child), th:not(:last-child),
td:not(:last-child) { td:not(:last-child) {
border-right: solid 1px transparent; border-right: solid 1px transparent;
......
module EE
module ProtectedRef
extend ActiveSupport::Concern
class_methods do
def protected_ref_access_levels(*types)
types.each do |type|
# We need to set `inverse_of` to make sure the `belongs_to`-object is set
# when creating children using `accepts_nested_attributes_for`.
#
# If we don't `protected_branch` or `protected_tag` would be empty and
# `project` cannot be delegated to it, which in turn would cause validations
# to fail.
has_many :"#{type}_access_levels", inverse_of: self.model_name.singular # rubocop:disable Cop/ActiveRecordDependent
accepts_nested_attributes_for :"#{type}_access_levels", allow_destroy: true
# Overwrite the validation for access levels
#
# EE Needs to allow more access levels in the relation:
# - 1 for each user/group
# - 1 with the `access_level` (Master, Developer)
validates :"#{type}_access_levels", length: { is: 1 }, if: -> { false }
# Returns access levels that grant the specified access type to the given user / group.
access_level_class = const_get("#{type}_access_level".classify)
protected_type = self.model_name.singular
scope(
:"#{type}_access_by_user",
-> (user) do
access_level_class.joins(protected_type.to_sym)
.where("#{protected_type}_id" => self.ids)
.merge(access_level_class.by_user(user))
end
)
scope(
:"#{type}_access_by_group",
-> (group) do
access_level_class.joins(protected_type.to_sym)
.where("#{protected_type}_id" => self.ids)
.merge(access_level_class.by_group(group))
end
)
end
end
end
end
end
...@@ -18,9 +18,15 @@ module EE ...@@ -18,9 +18,15 @@ module EE
validates :user_id, uniqueness: { scope: protected_type, allow_nil: true } validates :user_id, uniqueness: { scope: protected_type, allow_nil: true }
validates :access_level, uniqueness: { scope: protected_type, if: :role?, validates :access_level, uniqueness: { scope: protected_type, if: :role?,
conditions: -> { where(user_id: nil, group_id: nil) } } conditions: -> { where(user_id: nil, group_id: nil) } }
validates :group, :user,
absence: true,
unless: :protected_refs_for_users_required_and_available
scope :by_user, -> (user) { where(user: user ) } scope :by_user, -> (user) { where(user: user ) }
scope :by_group, -> (group) { where(group: group ) } scope :by_group, -> (group) { where(group: group ) }
scope :for_role, -> { where(user: nil, group: nil) }
scope :for_user, -> { where.not(user: nil) }
scope :for_group, -> { where.not(group: nil) }
end end
def type def type
...@@ -52,5 +58,13 @@ module EE ...@@ -52,5 +58,13 @@ module EE
super super
end end
# We don't need to validate the license if this access applies to a role.
#
# If it applies to a user/group we can only skip validation `nil`-validation
# if the feature is available
def protected_refs_for_users_required_and_available
type != :role && project.feature_available?(:protected_refs_for_users)
end
end end
end end
...@@ -17,17 +17,17 @@ module ProtectedRef ...@@ -17,17 +17,17 @@ module ProtectedRef
class_methods do class_methods do
def protected_ref_access_levels(*types) def protected_ref_access_levels(*types)
types.each do |type| types.each do |type|
has_many :"#{type}_access_levels", dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent # We need to set `inverse_of` to make sure the `belongs_to`-object is set
# when creating children using `accepts_nested_attributes_for`.
#
# If we don't `protected_branch` or `protected_tag` would be empty and
# `project` cannot be delegated to it, which in turn would cause validations
# to fail.
has_many :"#{type}_access_levels", dependent: :destroy, inverse_of: self.model_name.singular # rubocop:disable Cop/ActiveRecordDependent
validates :"#{type}_access_levels", length: { minimum: 0 } validates :"#{type}_access_levels", length: { is: 1, message: "are restricted to a single instance per #{self.model_name.human}." }
accepts_nested_attributes_for :"#{type}_access_levels", allow_destroy: true accepts_nested_attributes_for :"#{type}_access_levels", allow_destroy: true
# Returns access levels that grant the specified access type to the given user / group.
access_level_class = const_get("#{type}_access_level".classify)
protected_type = self.model_name.singular
scope :"#{type}_access_by_user", -> (user) { access_level_class.joins(protected_type.to_sym).where("#{protected_type}_id" => self.ids).merge(access_level_class.by_user(user)) }
scope :"#{type}_access_by_group", -> (group) { access_level_class.joins(protected_type.to_sym).where("#{protected_type}_id" => self.ids).merge(access_level_class.by_group(group)) }
end end
end end
......
...@@ -21,8 +21,9 @@ class License < ActiveRecord::Base ...@@ -21,8 +21,9 @@ class License < ActiveRecord::Base
MULTIPLE_ISSUE_ASSIGNEES_FEATURE = 'GitLab_MultipleIssueAssignees'.freeze MULTIPLE_ISSUE_ASSIGNEES_FEATURE = 'GitLab_MultipleIssueAssignees'.freeze
MULTIPLE_ISSUE_BOARDS_FEATURE = 'GitLab_MultipleIssueBoards'.freeze MULTIPLE_ISSUE_BOARDS_FEATURE = 'GitLab_MultipleIssueBoards'.freeze
OBJECT_STORAGE_FEATURE = 'GitLab_ObjectStorage'.freeze OBJECT_STORAGE_FEATURE = 'GitLab_ObjectStorage'.freeze
PROTECTED_REFS_FOR_USERS_FEATURE = 'GitLab_RefPermissionsForUsers'.freeze
PUSH_RULES_FEATURE = 'GitLab_PushRules'.freeze PUSH_RULES_FEATURE = 'GitLab_PushRules'.freeze
RELATED_ISSUES_FEATURE = 'GitLab_RelatedIssues'.freeze RELATED_ISSUES_FEATURE = 'RelatedIssues'.freeze
SERVICE_DESK_FEATURE = 'GitLab_ServiceDesk'.freeze SERVICE_DESK_FEATURE = 'GitLab_ServiceDesk'.freeze
VARIABLE_ENVIRONMENT_SCOPE_FEATURE = 'GitLab_VariableEnvironmentScope'.freeze VARIABLE_ENVIRONMENT_SCOPE_FEATURE = 'GitLab_VariableEnvironmentScope'.freeze
...@@ -52,6 +53,7 @@ class License < ActiveRecord::Base ...@@ -52,6 +53,7 @@ class License < ActiveRecord::Base
merge_request_squash: MERGE_REQUEST_SQUASH_FEATURE, merge_request_squash: MERGE_REQUEST_SQUASH_FEATURE,
multiple_issue_assignees: MULTIPLE_ISSUE_ASSIGNEES_FEATURE, multiple_issue_assignees: MULTIPLE_ISSUE_ASSIGNEES_FEATURE,
multiple_issue_boards: MULTIPLE_ISSUE_BOARDS_FEATURE, multiple_issue_boards: MULTIPLE_ISSUE_BOARDS_FEATURE,
protected_refs_for_users: PROTECTED_REFS_FOR_USERS_FEATURE,
push_rules: PUSH_RULES_FEATURE push_rules: PUSH_RULES_FEATURE
}.freeze }.freeze
...@@ -77,6 +79,7 @@ class License < ActiveRecord::Base ...@@ -77,6 +79,7 @@ class License < ActiveRecord::Base
{ MULTIPLE_ISSUE_ASSIGNEES_FEATURE => 1 }, { MULTIPLE_ISSUE_ASSIGNEES_FEATURE => 1 },
{ MULTIPLE_ISSUE_BOARDS_FEATURE => 1 }, { MULTIPLE_ISSUE_BOARDS_FEATURE => 1 },
{ PUSH_RULES_FEATURE => 1 }, { PUSH_RULES_FEATURE => 1 },
{ PROTECTED_REFS_FOR_USERS_FEATURE => 1 },
{ RELATED_ISSUES_FEATURE => 1 } { RELATED_ISSUES_FEATURE => 1 }
].freeze ].freeze
...@@ -122,6 +125,7 @@ class License < ActiveRecord::Base ...@@ -122,6 +125,7 @@ class License < ActiveRecord::Base
{ MULTIPLE_ISSUE_ASSIGNEES_FEATURE => 1 }, { MULTIPLE_ISSUE_ASSIGNEES_FEATURE => 1 },
{ MULTIPLE_ISSUE_BOARDS_FEATURE => 1 }, { MULTIPLE_ISSUE_BOARDS_FEATURE => 1 },
{ OBJECT_STORAGE_FEATURE => 1 }, { OBJECT_STORAGE_FEATURE => 1 },
{ PROTECTED_REFS_FOR_USERS_FEATURE => 1 },
{ PUSH_RULES_FEATURE => 1 }, { PUSH_RULES_FEATURE => 1 },
{ SERVICE_DESK_FEATURE => 1 } { SERVICE_DESK_FEATURE => 1 }
].freeze ].freeze
......
class ProtectedBranch < ActiveRecord::Base class ProtectedBranch < ActiveRecord::Base
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include ProtectedRef include ProtectedRef
prepend EE::ProtectedRef
protected_ref_access_levels :merge, :push protected_ref_access_levels :merge, :push
......
class ProtectedTag < ActiveRecord::Base class ProtectedTag < ActiveRecord::Base
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include ProtectedRef include ProtectedRef
include EE::ProtectedRef
protected_ref_access_levels :create protected_ref_access_levels :create
......
.panel.panel-default.protected-branches-list - can_admin_project = can?(current_user, :admin_project, @project)
- if @protected_branches.empty?
.panel-heading
%h3.panel-title
Protected branch (#{@protected_branches.size})
%p.settings-message.text-center
There are currently no protected branches, protect a branch with the form above.
- else
- can_admin_project = can?(current_user, :admin_project, @project)
%table.table.table-bordered = render layout: 'projects/protected_branches/shared/branches_list', locals: { can_admin_project: can_admin_project } do
%colgroup = render partial: 'projects/protected_branches/protected_branch', collection: @protected_branches, locals: { can_admin_project: can_admin_project}
%col{ width: "20%" }
%col{ width: "20%" }
%col{ width: "20%" }
%col{ width: "20%" }
- if can_admin_project
%col
%thead
%tr
%th Protected branch (#{@protected_branches.size})
%th Last commit
%th Allowed to merge
%th Allowed to push
- if can_admin_project
%th
%tbody
= render partial: 'projects/protected_branches/protected_branch', collection: @protected_branches, locals: { can_admin_project: can_admin_project}
= paginate @protected_branches, theme: 'gitlab'
= form_for [@project.namespace.becomes(Namespace), @project, @protected_branch], html: { class: 'new-protected-branch js-new-protected-branch' } do |f| - content_for :merge_access_levels do
.panel.panel-default .merge_access_levels-container
.panel-heading = dropdown_tag('Select',
%h3.panel-title options: { toggle_class: 'js-allowed-to-merge wide',
Protect a branch dropdown_class: 'dropdown-menu-selectable capitalize-header',
.panel-body data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes' }})
.form-horizontal - content_for :push_access_levels do
= form_errors(@protected_branch) .push_access_levels-container
.form-group = dropdown_tag('Select',
= f.label :name, class: 'col-md-2 text-right' do options: { toggle_class: 'js-allowed-to-push wide',
Branch: dropdown_class: 'dropdown-menu-selectable capitalize-header',
.col-md-10 data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }})
= render partial: "projects/protected_branches/dropdown", locals: { f: f }
.help-block
= link_to 'Wildcards', help_page_path('user/project/protected_branches', anchor: 'wildcard-protected-branches')
such as
%code *-stable
or
%code production/*
are supported
.form-group
%label.col-md-2.text-right{ for: 'merge_access_levels_attributes' }
Allowed to merge:
.col-md-10
.merge_access_levels-container
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-merge js-multiselect wide',
dropdown_class: 'dropdown-menu-user dropdown-menu-selectable capitalize-header', filter: true,
data: { input_id: 'merge_access_levels_attributes', default_label: 'Select' } })
.form-group
%label.col-md-2.text-right{ for: 'push_access_levels_attributes' }
Allowed to push:
.col-md-10
.push_access_levels-container
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-push js-multiselect wide',
dropdown_class: 'dropdown-menu-user dropdown-menu-selectable capitalize-header', filter: true,
data: { input_id: 'push_access_levels_attributes', default_label: 'Select' } })
.help-block
Only groups that
= link_to 'have this project shared', help_page_path('workflow/share_projects_with_other_groups')
can be added here
.panel-footer = render 'projects/protected_branches/shared/create_protected_branch'
= f.submit 'Protect', class: 'btn-create btn', disabled: true
- expanded = Rails.env.test?
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('protected_branches') = page_specific_javascript_bundle_tag('protected_branches')
%section.settings - content_for :create_protected_branch do
.settings-header = render 'projects/protected_branches/create_protected_branch'
%h4
Protected Branches
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
Keep stable branches secure and force developers to use merge requests.
.settings-content.no-animate{ class: ('expanded' if expanded) }
%p
By default, protected branches are designed to:
%ul
%li prevent their creation, if not already created, from everybody except Masters
%li prevent pushes from everybody except Masters
%li prevent <strong>anyone</strong> from force pushing to the branch
%li prevent <strong>anyone</strong> from deleting the branch
%p Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches"), class: "underlined-link"} and #{link_to "project permissions", help_page_path("user/permissions"), class: "underlined-link"}.
- if can? current_user, :admin_project, @project - content_for :branches_list do
= render 'projects/protected_branches/create_protected_branch' = render "projects/protected_branches/branches_list"
= render "projects/protected_branches/branches_list" = render 'projects/protected_branches/shared/index'
%tr.js-protected-branch-edit-form{ data: { url: project_protected_branch_path(@project, protected_branch) } } = render layout: 'projects/protected_branches/shared/protected_branch', locals: { protected_branch: protected_branch } do
%td = render partial: 'projects/protected_branches/ee/fallback_update_protected_branch', locals: { protected_branch: protected_branch }
%span.ref-name= protected_branch.name
- if @project.root_ref?(protected_branch.name)
%span.label.label-info.prepend-left-5 default
%td
- if protected_branch.wildcard?
- matching_branches = protected_branch.matching(repository.branches)
= link_to pluralize(matching_branches.count, "matching branch"), project_protected_branch_path(@project, protected_branch)
- else
- if commit = protected_branch.commit
= link_to(commit.short_id, project_commit_path(@project, commit.id), class: 'commit-sha')
= time_ago_with_tooltip(commit.committed_date)
- else
(branch was removed from repository)
= render partial: 'projects/protected_branches/protected_branch_access_summary', locals: { protected_branch: protected_branch }
- if can_admin_project
%td
= link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning'
%td
= render partial: 'projects/protected_branches/access_level_dropdown', locals: { protected_branch: protected_branch, access_levels: protected_branch.merge_access_levels, level_frequencies: access_level_frequencies(protected_branch.merge_access_levels), input_basic_name: 'merge_access_levels', toggle_class: 'js-allowed-to-merge' }
%td
= render partial: 'projects/protected_branches/access_level_dropdown', locals: { protected_branch: protected_branch, access_levels: protected_branch.push_access_levels, level_frequencies: access_level_frequencies(protected_branch.push_access_levels), input_basic_name: 'push_access_levels', toggle_class: 'js-allowed-to-push' }
%td %td
= hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_levels.first.access_level = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_levels.first.access_level
= dropdown_tag( (protected_branch.merge_access_levels.first.humanize || 'Select') , = dropdown_tag( (protected_branch.merge_access_levels.first.humanize || 'Select') ,
options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container capitalize-header', options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container capitalize-header',
data: { field_name: "allowed_to_merge_#{protected_branch.id}", access_level_id: protected_branch.merge_access_levels.first.id }}) data: { field_name: "allowed_to_merge_#{protected_branch.id}", access_level_id: protected_branch.merge_access_levels.first.id }})
%td %td
= hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_levels.first.access_level = hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_levels.first.access_level
= dropdown_tag( (protected_branch.push_access_levels.first.humanize || 'Select') , = dropdown_tag( (protected_branch.push_access_levels.first.humanize || 'Select') ,
options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header', options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header',
data: { field_name: "allowed_to_push_#{protected_branch.id}", access_level_id: protected_branch.push_access_levels.first.id }}) data: { field_name: "allowed_to_push_#{protected_branch.id}", access_level_id: protected_branch.push_access_levels.first.id }})
- can_admin_project = can?(current_user, :admin_project, @project)
= render layout: 'projects/protected_branches/shared/branches_list', locals: { can_admin_project: can_admin_project } do
= render partial: 'projects/protected_branches/ee/protected_branch', collection: @protected_branches, locals: { can_admin_project: can_admin_project}
- content_for :merge_access_levels do
.merge_access_levels-container
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-merge js-multiselect wide',
dropdown_class: 'dropdown-menu-user dropdown-menu-selectable capitalize-header', filter: true,
data: { input_id: 'merge_access_levels_attributes', default_label: 'Select' } })
- content_for :push_access_levels do
.push_access_levels-container
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-push js-multiselect wide',
dropdown_class: 'dropdown-menu-user dropdown-menu-selectable capitalize-header', filter: true,
data: { input_id: 'push_access_levels_attributes', default_label: 'Select' } })
.help-block
Only groups that
= link_to 'have this project shared', help_page_path('workflow/share_projects_with_other_groups')
can be added here
= render 'projects/protected_branches/shared/create_protected_branch'
- merge_access_level = protected_branch.merge_access_levels.for_role.first
- push_access_level = protected_branch.push_access_levels.for_role.first
- user_merge_access_levels = protected_branch.merge_access_levels.for_user
- user_push_access_levels = protected_branch.push_access_levels.for_user
- group_merge_access_levels = protected_branch.merge_access_levels.for_group
- group_push_access_levels = protected_branch.push_access_levels.for_group
%td
= hidden_field_tag "allowed_to_merge_#{protected_branch.id}", merge_access_level&.access_level
= dropdown_tag( (merge_access_level&.humanize || 'Select') ,
options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container capitalize-header',
data: { field_name: "allowed_to_merge_#{protected_branch.id}", access_level_id: merge_access_level&.id }})
- if user_merge_access_levels.any?
%p.small
The following
#{ 'user'.pluralize(user_merge_access_levels.size) }
can also merge into this branch:
#{ user_merge_access_levels.map(&:humanize).to_sentence }
- if group_merge_access_levels.any?
%p.small
Members of
#{ group_merge_access_levels.size > 1 ? 'these groups' : 'this group' }
can also merge into this branch:
#{ group_merge_access_levels.map(&:humanize).to_sentence }
%td
= hidden_field_tag "allowed_to_push_#{protected_branch.id}", push_access_level&.access_level
= dropdown_tag( (push_access_level&.humanize || 'Select') ,
options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header',
data: { field_name: "allowed_to_push_#{protected_branch.id}", access_level_id: push_access_level&.id }})
- if user_push_access_levels.any?
%p.small
The following
#{ 'user'.pluralize(user_push_access_levels.size) }
can also push to this branch:
#{ user_push_access_levels.map(&:humanize).to_sentence }
- if group_push_access_levels.any?
%p.small
Members of
#{ group_push_access_levels.size > 1 ? 'these groups' : 'this group' }
can also push to this branch:
#{ group_push_access_levels.map(&:humanize).to_sentence }
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('ee_protected_branches')
- content_for :create_protected_branch do
= render 'projects/protected_branches/ee/create_protected_branch'
- content_for :branches_list do
= render "projects/protected_branches/ee/branches_list"
= render 'projects/protected_branches/shared/index'
= render layout: 'projects/protected_branches/shared/protected_branch', locals: { protected_branch: protected_branch } do
= render partial: 'projects/protected_branches/ee/protected_branch_access_summary', locals: { protected_branch: protected_branch }
%td
= render partial: 'projects/protected_branches/ee/access_level_dropdown', locals: { protected_branch: protected_branch, access_levels: protected_branch.merge_access_levels, level_frequencies: access_level_frequencies(protected_branch.merge_access_levels), input_basic_name: 'merge_access_levels', toggle_class: 'js-allowed-to-merge' }
%td
= render partial: 'projects/protected_branches/ee/access_level_dropdown', locals: { protected_branch: protected_branch, access_levels: protected_branch.push_access_levels, level_frequencies: access_level_frequencies(protected_branch.push_access_levels), input_basic_name: 'push_access_levels', toggle_class: 'js-allowed-to-push' }
.panel.panel-default.protected-branches-list
- if @protected_branches.empty?
.panel-heading
%h3.panel-title
Protected branch (#{@protected_branches.size})
%p.settings-message.text-center
There are currently no protected branches, protect a branch with the form above.
- else
%table.table.table-bordered
%colgroup
%col{ width: "20%" }
%col{ width: "20%" }
%col{ width: "20%" }
%col{ width: "20%" }
- if can_admin_project
%col
%thead
%tr
%th Protected branch (#{@protected_branches.size})
%th Last commit
%th Allowed to merge
%th Allowed to push
- if can_admin_project
%th
%tbody
= yield
= paginate @protected_branches, theme: 'gitlab'
= form_for [@project.namespace.becomes(Namespace), @project, @protected_branch] do |f|
.panel.panel-default
.panel-heading
%h3.panel-title
Protect a branch
.panel-body
.form-horizontal
= form_errors(@protected_branch)
.form-group
= f.label :name, class: 'col-md-2 text-right' do
Branch:
.col-md-10
= render partial: "projects/protected_branches/shared/dropdown", locals: { f: f }
.help-block
= link_to 'Wildcards', help_page_path('user/project/protected_branches', anchor: 'wildcard-protected-branches')
such as
%code *-stable
or
%code production/*
are supported
.form-group
%label.col-md-2.text-right{ for: 'merge_access_levels_attributes' }
Allowed to merge:
.col-md-10
= yield :merge_access_levels
.form-group
%label.col-md-2.text-right{ for: 'push_access_levels_attributes' }
Allowed to push:
.col-md-10
= yield :push_access_levels
.panel-footer
= f.submit 'Protect', class: 'btn-create btn', disabled: true
= f.hidden_field(:name)
= dropdown_tag('Select branch or create wildcard',
options: { toggle_class: 'js-protected-branch-select js-filter-submit wide git-revision-dropdown-toggle',
filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: "Search protected branches",
footer_content: true,
data: { show_no: true, show_any: true, show_upcoming: true,
selected: params[:protected_branch_name],
project_id: @project.try(:id) } }) do
%ul.dropdown-footer-list
%li
%button{ class: "create-new-protected-branch-button js-create-new-protected-branch", title: "New Protected Branch" }
Create wildcard
%code
- expanded = Rails.env.test?
%section.settings
.settings-header
%h4
Protected Branches
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
Keep stable branches secure and force developers to use merge requests.
.settings-content.no-animate{ class: ('expanded' if expanded) }
%p
By default, protected branches are designed to:
%ul
%li prevent their creation, if not already created, from everybody except Masters
%li prevent pushes from everybody except Masters
%li prevent <strong>anyone</strong> from force pushing to the branch
%li prevent <strong>anyone</strong> from deleting the branch
%p Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches"), class: "underlined-link"} and #{link_to "project permissions", help_page_path("user/permissions"), class: "underlined-link"}.
- if can? current_user, :admin_project, @project
= content_for :create_protected_branch
= content_for :branches_list
- can_admin_project = can?(current_user, :admin_project, @project)
%tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch) } }
%td
%span.ref-name= protected_branch.name
- if @project.root_ref?(protected_branch.name)
%span.label.label-info.prepend-left-5 default
%td
- if protected_branch.wildcard?
- matching_branches = protected_branch.matching(repository.branches)
= link_to pluralize(matching_branches.count, "matching branch"), namespace_project_protected_branch_path(@project.namespace, @project, protected_branch)
- else
- if commit = protected_branch.commit
= link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit-sha')
= time_ago_with_tooltip(commit.committed_date)
- else
(branch was removed from repository)
= yield
- if can_admin_project
%td
= link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning'
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
%th Last commit %th Last commit
%tbody %tbody
- @matching_refs.each do |matching_branch| - @matching_refs.each do |matching_branch|
= render partial: "matching_branch", object: matching_branch = render partial: "projects/protected_branches/shared/matching_branch", object: matching_branch
- else - else
%p.settings-message.text-center %p.settings-message.text-center
Couldn't find any matching branches. Couldn't find any matching branches.
= form_for [@project.namespace.becomes(Namespace), @project, @protected_tag], html: { class: 'new-protected-tag js-new-protected-tag' } do |f| - content_for :create_access_levels do
.panel.panel-default .create_access_levels-container
.panel-heading = dropdown_tag('Select',
%h3.panel-title options: { toggle_class: 'js-allowed-to-create wide',
Protect a tag dropdown_class: 'dropdown-menu-selectable',
.panel-body data: { field_name: 'protected_tag[create_access_levels_attributes][0][access_level]', input_id: 'create_access_levels_attributes' }})
.form-horizontal
= form_errors(@protected_tag)
.form-group
= f.label :name, class: 'col-md-2 text-right' do
Tag:
.col-md-10.protected-tags-dropdown
= render partial: "projects/protected_tags/dropdown", locals: { f: f }
.help-block
= link_to 'Wildcards', help_page_path('user/project/protected_tags', anchor: 'wildcard-protected-tags')
such as
%code v*
or
%code *-release
are supported
.form-group
%label.col-md-2.text-right{ for: 'create_access_levels_attributes' }
Allowed to create:
.col-md-10
.create_access_levels-container
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-create js-multiselect wide',
dropdown_class: 'dropdown-menu-user dropdown-menu-selectable', filter: true,
data: { input_id: 'create_access_levels_attributes', default_label: 'Select' } })
.help-block
Only groups that
= link_to 'have this project shared', help_page_path('workflow/share_projects_with_other_groups')
can be added here
.panel-footer = render 'projects/protected_tags/shared/create_protected_tag'
= f.submit 'Protect', class: 'btn-create btn', disabled: true
- expanded = Rails.env.test?
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('protected_tags') = page_specific_javascript_bundle_tag('protected_tags')
%section.settings.js-protected-tags-container{ data: { "groups-autocomplete" => "#{autocomplete_project_groups_path(format: :json)}", "users-autocomplete" => "#{autocomplete_users_path(format: :json)}" } } - content_for :create_protected_tag do
.settings-header = render 'projects/protected_tags/create_protected_tag'
%h4
Protected Tags
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
Limit access to creating and updating tags.
.settings-content.no-animate{ class: ('expanded' if expanded) }
%p
By default, protected tags are designed to:
%ul
%li Prevent tag creation by everybody except Masters
%li Prevent <strong>anyone</strong> from updating the tag
%li Prevent <strong>anyone</strong> from deleting the tag
%p Read more about #{link_to "protected tags", help_page_path("user/project/protected_tags"), class: "underlined-link"}. - content_for :tag_list do
= render "projects/protected_tags/tags_list"
- if can? current_user, :admin_project, @project = render 'projects/protected_tags/shared/index'
= render 'projects/protected_tags/create_protected_tag'
= render "projects/protected_tags/tags_list"
%tr.js-protected-tag-edit-form{ data: { url: project_protected_tag_path(@project, protected_tag) } } = render layout: 'projects/protected_tags/shared/protected_tag', locals: { protected_tag: protected_tag } do
%td = render partial: 'projects/protected_tags/ee/fallback_update_protected_tag', locals: { protected_tag: protected_tag }
%span.ref-name= protected_tag.name
- if @project.root_ref?(protected_tag.name)
%span.label.label-info.prepend-left-5 default
%td
- if protected_tag.wildcard?
- matching_tags = protected_tag.matching(repository.tags)
= link_to pluralize(matching_tags.count, "matching tag"), project_protected_tag_path(@project, protected_tag)
- else
- if commit = protected_tag.commit
= link_to(commit.short_id, project_commit_path(@project, commit.id), class: 'commit-sha')
= time_ago_with_tooltip(commit.committed_date)
- else
(tag was removed from repository)
= render partial: 'projects/protected_tags/protected_tag_access_summary', locals: { protected_tag: protected_tag }
- if can_admin_project
%td
= link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_tag], data: { confirm: 'Tag will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning'
%td
= render partial: 'projects/protected_tags/access_level_dropdown', locals: { protected_tag: protected_tag, access_levels: protected_tag.create_access_levels, level_frequencies: access_level_frequencies(protected_tag.create_access_levels), input_basic_name: 'create_access_levels', toggle_class: 'js-allowed-to-create' }
.panel.panel-default.protected-tags-list - can_admin_project = can?(current_user, :admin_project, @project)
- if @protected_tags.empty?
.panel-heading
%h3.panel-title
Protected tag (#{@protected_tags.size})
%p.settings-message.text-center
There are currently no protected tags, protect a tag with the form above.
- else
- can_admin_project = can?(current_user, :admin_project, @project)
%table.table.table-bordered = render layout: 'projects/protected_tags/shared/tags_list' do
%colgroup = render partial: 'projects/protected_tags/protected_tag', collection: @protected_tags, locals: { can_admin_project: can_admin_project}
%col{ width: "25%" }
%col{ width: "25%" }
%col{ width: "50%" }
- if can_admin_project
%col
%thead
%tr
%th Protected tag (#{@protected_tags.size})
%th Last commit
%th Allowed to create
- if can_admin_project
%th
%tbody
%tr
%td.flash-container{ colspan: 4 }
= render partial: 'projects/protected_tags/protected_tag', collection: @protected_tags, locals: { can_admin_project: can_admin_project}
= paginate @protected_tags, theme: 'gitlab'
- content_for :create_access_levels do
.create_access_levels-container
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-create js-multiselect wide',
dropdown_class: 'dropdown-menu-user dropdown-menu-selectable', filter: true,
data: { input_id: 'create_access_levels_attributes', default_label: 'Select' } })
.help-block
Only groups that
= link_to 'have this project shared', help_page_path('workflow/share_projects_with_other_groups')
can be added here
= render 'projects/protected_tags/shared/create_protected_tag'
- create_access_level = protected_tag.create_access_levels.for_role.first
- user_create_access_levels = protected_tag.create_access_levels.for_user
- group_create_access_levels =protected_tag.create_access_levels.for_group
%td
= hidden_field_tag "allowed_to_create_#{protected_tag.id}", create_access_level&.access_level
= dropdown_tag( (create_access_level&.humanize || 'Select') ,
options: { toggle_class: 'js-allowed-to-create', dropdown_class: 'dropdown-menu-selectable capitalize-header js-allowed-to-create-container',
data: { field_name: "allowed_to_create_#{protected_tag.id}", access_level_id: create_access_level&.id }})
- if user_create_access_levels.any?
%p.small
The following
#{ 'user'.pluralize(user_create_access_levels.size) }
can also create tags:
#{ user_create_access_levels.map(&:humanize).to_sentence }
- if group_create_access_levels.any?
%p.small
Members of
#{ group_create_access_levels.size > 1 ? 'these groups' : 'this group' }
can also create tags:
#{ group_create_access_levels.map(&:humanize).to_sentence }
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('ee_protected_tags')
- content_for :create_protected_tag do
= render 'projects/protected_tags/ee/create_protected_tag'
- content_for :tag_list do
= render "projects/protected_tags/ee/tags_list"
= render 'projects/protected_tags/shared/index'
= render layout: 'projects/protected_tags/shared/protected_tag', locals: { protected_tag: protected_tag } do
= render partial: 'projects/protected_tags/ee/protected_tag_access_summary', locals: { protected_tag: protected_tag }
%td
= render partial: 'projects/protected_tags/ee/access_level_dropdown', locals: { protected_tag: protected_tag, access_levels: protected_tag.create_access_levels, level_frequencies: access_level_frequencies(protected_tag.create_access_levels), input_basic_name: 'create_access_levels', toggle_class: 'js-allowed-to-create' }
- can_admin_project = can?(current_user, :admin_project, @project)
= render layout: 'projects/protected_tags/shared/tags_list' do
= render partial: 'projects/protected_tags/ee/protected_tag', collection: @protected_tags, locals: { can_admin_project: can_admin_project }
= form_for [@project.namespace.becomes(Namespace), @project, @protected_tag], html: { class: 'new-protected-tag js-new-protected-tag' } do |f|
.panel.panel-default
.panel-heading
%h3.panel-title
Protect a tag
.panel-body
.form-horizontal
= form_errors(@protected_tag)
.form-group
= f.label :name, class: 'col-md-2 text-right' do
Tag:
.col-md-10.protected-tags-dropdown
= render partial: "projects/protected_tags/shared/dropdown", locals: { f: f }
.help-block
= link_to 'Wildcards', help_page_path('user/project/protected_tags', anchor: 'wildcard-protected-tags')
such as
%code v*
or
%code *-release
are supported
.form-group
%label.col-md-2.text-right{ for: 'create_access_levels_attributes' }
Allowed to create:
.col-md-10
.create_access_levels-container
= yield :create_access_levels
.panel-footer
= f.submit 'Protect', class: 'btn-create btn', disabled: true
- expanded = Rails.env.test?
%section.settings
.settings-header
%h4
Protected Tags
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
Limit access to creating and updating tags.
.settings-content.no-animate{ class: ('expanded' if expanded) }
%p
By default, protected tags are designed to:
%ul
%li Prevent tag creation by everybody except Masters
%li Prevent <strong>anyone</strong> from updating the tag
%li Prevent <strong>anyone</strong> from deleting the tag
%p Read more about #{link_to "protected tags", help_page_path("user/project/protected_tags"), class: "underlined-link"}.
- if can? current_user, :admin_project, @project
= yield :create_protected_tag
= yield :tag_list
%tr.js-protected-tag-edit-form{ data: { url: project_protected_tag_path(@project, protected_tag) } }
%td
%span.ref-name= protected_tag.name
- if @project.root_ref?(protected_tag.name)
%span.label.label-info.prepend-left-5 default
%td
- if protected_tag.wildcard?
- matching_tags = protected_tag.matching(repository.tags)
= link_to pluralize(matching_tags.count, "matching tag"), project_protected_tag_path(@project, protected_tag)
- else
- if commit = protected_tag.commit
= link_to(commit.short_id, project_commit_path(@project, commit.id), class: 'commit-sha')
= time_ago_with_tooltip(commit.committed_date)
- else
(tag was removed from repository)
= yield
- if can? current_user, :admin_project, @project
%td
= link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_tag], data: { confirm: 'Tag will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning'
.panel.panel-default.protected-tags-list
- if @protected_tags.empty?
.panel-heading
%h3.panel-title
Protected tag (#{@protected_tags.size})
%p.settings-message.text-center
There are currently no protected tags, protect a tag with the form above.
- else
- can_admin_project = can?(current_user, :admin_project, @project)
%table.table.table-bordered
%colgroup
%col{ width: "25%" }
%col{ width: "25%" }
%col{ width: "50%" }
- if can_admin_project
%col
%thead
%tr
%th Protected tag (#{@protected_tags.size})
%th Last commit
%th Allowed to create
- if can_admin_project
%th
%tbody
%tr
%td.flash-container{ colspan: 4 }
= yield
= paginate @protected_tags, theme: 'gitlab'
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
%th Last commit %th Last commit
%tbody %tbody
- @matching_refs.each do |matching_tag| - @matching_refs.each do |matching_tag|
= render partial: "matching_tag", object: matching_tag = render partial: "projects/protected_tags/shared/matching_tag", object: matching_tag
- else - else
%p.settings-message.text-center %p.settings-message.text-center
Couldn't find any matching tags. Couldn't find any matching tags.
...@@ -9,6 +9,15 @@ ...@@ -9,6 +9,15 @@
= render "projects/push_rules/index" = render "projects/push_rules/index"
= render "projects/mirrors/show" = render "projects/mirrors/show"
= render "projects/protected_branches/index" -# Protected branches & tags use a lot of nested partials.
= render "projects/protected_tags/index" -# The shared parts of the views can be found in the `shared` directory.
-# Those are used throughout the actual views. These `shared` views are then
-# reused in EE.
- if @project.feature_available?(:protected_refs_for_users, current_user)
= render "projects/protected_branches/ee/index"
= render "projects/protected_tags/ee/index"
- else
= render "projects/protected_branches/index"
= render "projects/protected_tags/index"
= render @deploy_keys = render @deploy_keys
...@@ -60,7 +60,9 @@ var config = { ...@@ -60,7 +60,9 @@ var config = {
profile: './profile/profile_bundle.js', profile: './profile/profile_bundle.js',
prometheus_metrics: './prometheus_metrics', prometheus_metrics: './prometheus_metrics',
protected_branches: './protected_branches/protected_branches_bundle.js', protected_branches: './protected_branches/protected_branches_bundle.js',
ee_protected_branches: './protected_branches/ee/protected_branches_bundle.js',
protected_tags: './protected_tags', protected_tags: './protected_tags',
ee_protected_tags: './protected_tags/ee',
service_desk: './projects/settings_service_desk/service_desk_bundle.js', service_desk: './projects/settings_service_desk/service_desk_bundle.js',
sidebar: './sidebar/sidebar_bundle.js', sidebar: './sidebar/sidebar_bundle.js',
schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js', schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js',
......
...@@ -102,6 +102,67 @@ feature 'Protected Branches', feature: true, js: true do ...@@ -102,6 +102,67 @@ feature 'Protected Branches', feature: true, js: true do
end end
describe "access control" do describe "access control" do
include_examples "protected branches > access control > EE" describe 'with ref permissions for users enabled' do
before do
stub_licensed_features(protected_refs_for_users: true)
end
include_examples "protected branches > access control > EE"
end
describe 'with ref permissions for users disabled' do
before do
stub_licensed_features(protected_refs_for_users: false)
end
include_examples "protected branches > access control > CE"
context 'with existing access levels' do
let(:protected_branch) { create(:protected_branch, project: project) }
it 'shows users that can push to the branch' do
protected_branch.push_access_levels.new(user: create(:user, name: 'Jane'))
.save!(validate: false)
visit project_settings_repository_path(project)
expect(page).to have_content("The following user can also push to this branch: "\
"Jane")
end
it 'shows groups that can push to the branch' do
protected_branch.push_access_levels.new(group: create(:group, name: 'Team Awesome'))
.save!(validate: false)
visit project_settings_repository_path(project)
expect(page).to have_content("Members of this group can also push to "\
"this branch: Team Awesome")
end
it 'shows users that can merge into the branch' do
protected_branch.merge_access_levels.new(user: create(:user, name: 'Jane'))
.save!(validate: false)
visit project_settings_repository_path(project)
expect(page).to have_content("The following user can also merge into "\
"this branch: Jane")
end
it 'shows groups that have can push to the branch' do
protected_branch.merge_access_levels.new(group: create(:group, name: 'Team Awesome'))
.save!(validate: false)
protected_branch.merge_access_levels.new(group: create(:group, name: 'Team B'))
.save!(validate: false)
visit project_settings_repository_path(project)
expect(page).to have_content("Members of these groups can also merge into "\
"this branch:")
expect(page).to have_content(/(Team Awesome|Team B) and (Team Awesome|Team B)/)
end
end
end
end end
end end
...@@ -106,6 +106,44 @@ feature 'Projected Tags', feature: true, js: true do ...@@ -106,6 +106,44 @@ feature 'Projected Tags', feature: true, js: true do
end end
describe "access control" do describe "access control" do
include_examples "protected tags > access control > EE" describe 'with ref permissions for users enabled' do
before do
stub_licensed_features(protected_refs_for_users: true)
end
include_examples "protected tags > access control > EE"
end
describe 'with ref permissions for users disabled' do
before do
stub_licensed_features(protected_refs_for_users: false)
end
include_examples "protected tags > access control > CE"
describe 'with existing access levels' do
let(:protected_tag) { create(:protected_tag, project: project) }
it 'shows users that can push to the branch' do
protected_tag.create_access_levels.new(user: create(:user, name: 'Jane'))
.save!(validate: false)
visit project_settings_repository_path(project)
expect(page).to have_content("The following user can also create tags: "\
"Jane")
end
it 'shows groups that can create to the branch' do
protected_tag.create_access_levels.new(group: create(:group, name: 'Team Awesome'))
.save!(validate: false)
visit project_settings_repository_path(project)
expect(page).to have_content("Members of this group can also create tags: "\
"Team Awesome")
end
end
end
end end
end end
require 'spec_helper'
describe EE::ProtectedRefAccess do
included_in_classes = [ProtectedBranch::MergeAccessLevel,
ProtectedBranch::PushAccessLevel,
ProtectedTag::CreateAccessLevel]
included_in_classes.each do |included_in_class|
context "in #{included_in_class}" do
let(:access_level) do
factory_name = included_in_class.name.underscore.tr('/', '_')
build(factory_name)
end
it "#{included_in_class} includes {described_class}" do
expect(included_in_class.included_modules).to include(described_class)
end
context 'with the `protected_refs_for_users` feature disabled' do
before do
stub_licensed_features(protected_refs_for_users: false)
end
it "allows creating an #{included_in_class} with a group" do
access_level.group = create(:group)
expect(access_level).not_to be_valid
expect(access_level.errors).to include(:group)
end
it "allows creating an #{included_in_class} with a user" do
access_level.user = create(:user)
expect(access_level).not_to be_valid
expect(access_level.errors).to include(:user)
end
end
context 'with the `protected_refs_for_users` feature enabled' do
before do
stub_licensed_features(protected_refs_for_users: true)
end
it "allows creating an #{included_in_class} with a group" do
access_level.group = create(:group)
expect(access_level).to be_valid
end
it "allows creating an #{included_in_class} with a user" do
access_level.user = create(:user)
expect(access_level).to be_valid
end
end
end
end
end
require 'spec_helper'
describe EE::ProtectedRef do
context 'for protected branches' do
it 'deletes all related access levels' do
protected_branch = create(:protected_branch)
2.times { protected_branch.merge_access_levels.create!(group: create(:group)) }
2.times { protected_branch.push_access_levels.create!(user: create(:user)) }
protected_branch.destroy
expect(ProtectedBranch::MergeAccessLevel.count).to be(0)
expect(ProtectedBranch::PushAccessLevel.count).to be(0)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment