Commit a5ab3467 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent eb30dd6e
...@@ -3,9 +3,12 @@ ...@@ -3,9 +3,12 @@
import Members from 'ee_else_ce/members'; import Members from 'ee_else_ce/members';
import memberExpirationDate from '~/member_expiration_date'; import memberExpirationDate from '~/member_expiration_date';
import UsersSelect from '~/users_select'; import UsersSelect from '~/users_select';
import groupsSelect from '~/groups_select';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
memberExpirationDate(); memberExpirationDate();
memberExpirationDate('.js-access-expiration-date-groups');
new Members(); new Members();
groupsSelect();
new UsersSelect(); new UsersSelect();
}); });
<script> <script>
import { mapActions } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { GlFormGroup, GlToggle, GlFormSelect, GlFormTextarea, GlButton } from '@gitlab/ui'; import { GlFormGroup, GlToggle, GlFormSelect, GlFormTextarea, GlButton, GlCard } from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale'; import { s__, __, sprintf } from '~/locale';
import { NAME_REGEX_LENGTH } from '../constants'; import { NAME_REGEX_LENGTH } from '../constants';
import { mapComputed } from '~/vuex_shared/bindings'; import { mapComputed } from '~/vuex_shared/bindings';
...@@ -12,19 +12,25 @@ export default { ...@@ -12,19 +12,25 @@ export default {
GlFormSelect, GlFormSelect,
GlFormTextarea, GlFormTextarea,
GlButton, GlButton,
GlCard,
}, },
labelsConfig: { labelsConfig: {
cols: 3, cols: 3,
align: 'right', align: 'right',
}, },
computed: { computed: {
...mapComputed('settings', 'updateSettings', [ ...mapState(['formOptions']),
'enabled', ...mapComputed(
'cadence', [
'older_than', 'enabled',
'keep_n', { key: 'cadence', getter: 'getCadence' },
'name_regex', { key: 'older_than', getter: 'getOlderThan' },
]), { key: 'keep_n', getter: 'getKeepN' },
'name_regex',
],
'updateSettings',
'settings',
),
policyEnabledText() { policyEnabledText() {
return this.enabled ? __('enabled') : __('disabled'); return this.enabled ? __('enabled') : __('disabled');
}, },
...@@ -66,12 +72,12 @@ export default { ...@@ -66,12 +72,12 @@ export default {
</script> </script>
<template> <template>
<div class="card"> <form ref="form-element" @submit.prevent="saveSettings" @reset.prevent="resetSettings">
<form ref="form-element" @submit.prevent="saveSettings" @reset.prevent="resetSettings"> <gl-card>
<div class="card-header"> <template #header>
{{ s__('ContainerRegistry|Tag expiration policy') }} {{ s__('ContainerRegistry|Tag expiration policy') }}
</div> </template>
<div class="card-body"> <template>
<gl-form-group <gl-form-group
id="expiration-policy-toggle-group" id="expiration-policy-toggle-group"
:label-cols="$options.labelsConfig.cols" :label-cols="$options.labelsConfig.cols"
...@@ -92,9 +98,10 @@ export default { ...@@ -92,9 +98,10 @@ export default {
label-for="expiration-policy-interval" label-for="expiration-policy-interval"
:label="s__('ContainerRegistry|Expiration interval:')" :label="s__('ContainerRegistry|Expiration interval:')"
> >
<gl-form-select id="expiration-policy-interval" v-model="older_than"> <gl-form-select id="expiration-policy-interval" v-model="older_than" :disabled="!enabled">
<option value="1">{{ __('Option 1') }}</option> <option v-for="option in formOptions.olderThan" :key="option.key" :value="option.key">
<option value="2">{{ __('Option 2') }}</option> {{ option.label }}
</option>
</gl-form-select> </gl-form-select>
</gl-form-group> </gl-form-group>
...@@ -105,9 +112,10 @@ export default { ...@@ -105,9 +112,10 @@ export default {
label-for="expiration-policy-schedule" label-for="expiration-policy-schedule"
:label="s__('ContainerRegistry|Expiration schedule:')" :label="s__('ContainerRegistry|Expiration schedule:')"
> >
<gl-form-select id="expiration-policy-schedule" v-model="cadence"> <gl-form-select id="expiration-policy-schedule" v-model="cadence" :disabled="!enabled">
<option value="1">{{ __('Option 1') }}</option> <option v-for="option in formOptions.cadence" :key="option.key" :value="option.key">
<option value="2">{{ __('Option 2') }}</option> {{ option.label }}
</option>
</gl-form-select> </gl-form-select>
</gl-form-group> </gl-form-group>
...@@ -118,9 +126,10 @@ export default { ...@@ -118,9 +126,10 @@ export default {
label-for="expiration-policy-latest" label-for="expiration-policy-latest"
:label="s__('ContainerRegistry|Expiration latest:')" :label="s__('ContainerRegistry|Expiration latest:')"
> >
<gl-form-select id="expiration-policy-latest" v-model="keep_n"> <gl-form-select id="expiration-policy-latest" v-model="keep_n" :disabled="!enabled">
<option value="1">{{ __('Option 1') }}</option> <option v-for="option in formOptions.keepN" :key="option.key" :value="option.key">
<option value="2">{{ __('Option 2') }}</option> {{ option.label }}
</option>
</gl-form-select> </gl-form-select>
</gl-form-group> </gl-form-group>
...@@ -140,19 +149,30 @@ export default { ...@@ -140,19 +149,30 @@ export default {
v-model="name_regex" v-model="name_regex"
:placeholder="nameRegexPlaceholder" :placeholder="nameRegexPlaceholder"
:state="nameRegexState" :state="nameRegexState"
:disabled="!enabled"
trim trim
/> />
<template #description> <template #description>
<span ref="regex-description" v-html="regexHelpText"></span> <span ref="regex-description" v-html="regexHelpText"></span>
</template> </template>
</gl-form-group> </gl-form-group>
</div> </template>
<div class="card-footer text-right"> <template #footer>
<gl-button ref="cancel-button" type="reset">{{ __('Cancel') }}</gl-button> <div class="d-flex justify-content-end">
<gl-button ref="save-button" type="submit" :disabled="formIsValid" variant="success"> <gl-button ref="cancel-button" type="reset" class="mr-2 d-block">{{
{{ __('Save Expiration Policy') }} __('Cancel')
</gl-button> }}</gl-button>
</div> <gl-button
</form> ref="save-button"
</div> type="submit"
:disabled="formIsValid"
variant="success"
class="d-block"
>
{{ __('Save expiration policy') }}
</gl-button>
</div>
</template>
</gl-card>
</form>
</template> </template>
import { findDefaultOption } from '../utils';
export const getCadence = state =>
state.settings.cadence || findDefaultOption(state.formOptions.cadence);
export const getKeepN = state =>
state.settings.keep_n || findDefaultOption(state.formOptions.keepN);
export const getOlderThan = state =>
state.settings.older_than || findDefaultOption(state.formOptions.olderThan);
...@@ -2,6 +2,7 @@ import Vue from 'vue'; ...@@ -2,6 +2,7 @@ import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import * as actions from './actions'; import * as actions from './actions';
import mutations from './mutations'; import mutations from './mutations';
import * as getters from './getters';
import state from './state'; import state from './state';
Vue.use(Vuex); Vue.use(Vuex);
...@@ -11,6 +12,7 @@ export const createStore = () => ...@@ -11,6 +12,7 @@ export const createStore = () =>
state, state,
actions, actions,
mutations, mutations,
getters,
}); });
export default createStore(); export default createStore();
...@@ -3,6 +3,11 @@ import * as types from './mutation_types'; ...@@ -3,6 +3,11 @@ import * as types from './mutation_types';
export default { export default {
[types.SET_INITIAL_STATE](state, initialState) { [types.SET_INITIAL_STATE](state, initialState) {
state.projectId = initialState.projectId; state.projectId = initialState.projectId;
state.formOptions = {
cadence: JSON.parse(initialState.cadenceOptions),
keepN: JSON.parse(initialState.keepNOptions),
olderThan: JSON.parse(initialState.olderThanOptions),
};
}, },
[types.UPDATE_SETTINGS](state, settings) { [types.UPDATE_SETTINGS](state, settings) {
state.settings = { ...state.settings, ...settings }; state.settings = { ...state.settings, ...settings };
......
...@@ -23,4 +23,8 @@ export default () => ({ ...@@ -23,4 +23,8 @@ export default () => ({
* Same structure as settings, above but Frozen object and used only in case the user clicks 'cancel' * Same structure as settings, above but Frozen object and used only in case the user clicks 'cancel'
*/ */
original: {}, original: {},
/*
* Contains the options used to populate the form selects
*/
formOptions: {},
}); });
export const findDefaultOption = options => {
const item = options.find(o => o.default);
return item ? item.key : null;
};
export default () => {};
export const mapComputed = (root, updateFn, list) => { /**
* Returns computed properties two way bound to vuex
*
* @param {(string[]|Object[])} list - list of string matching state keys or list objects
* @param {string} list[].key - the key matching the key present in the vuex state
* @param {string} list[].getter - the name of the getter, leave it empty to not use a getter
* @param {string} list[].updateFn - the name of the action, leave it empty to use the default action
* @param {string} defaultUpdateFn - the default function to dispatch
* @param {string} root - the key of the state where to search fo they keys described in list
* @returns {Object} a dictionary with all the computed properties generated
*/
export const mapComputed = (list, defaultUpdateFn, root) => {
const result = {}; const result = {};
list.forEach(key => { list.forEach(item => {
const [getter, key, updateFn] =
typeof item === 'string'
? [false, item, defaultUpdateFn]
: [item.getter, item.key, item.updateFn || defaultUpdateFn];
result[key] = { result[key] = {
get() { get() {
return this.$store.state[root][key]; if (getter) {
return this.$store.getters[getter];
} else if (root) {
return this.$store.state[root][key];
}
return this.$store.state[key];
}, },
set(value) { set(value) {
this.$store.dispatch(updateFn, { [key]: value }); this.$store.dispatch(updateFn, { [key]: value });
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
} }
.users-project-form { .invite-users-form {
.btn-success { .btn-success {
margin-right: 10px; margin-right: 10px;
} }
......
...@@ -5,6 +5,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -5,6 +5,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
before_action :set_application_setting before_action :set_application_setting
before_action :whitelist_query_limiting, only: [:usage_data] before_action :whitelist_query_limiting, only: [:usage_data]
before_action :validate_self_monitoring_feature_flag_enabled, only: [
:create_self_monitoring_project,
:status_create_self_monitoring_project,
:delete_self_monitoring_project,
:status_delete_self_monitoring_project
]
before_action do before_action do
push_frontend_feature_flag(:self_monitoring_project) push_frontend_feature_flag(:self_monitoring_project)
...@@ -74,8 +80,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -74,8 +80,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end end
def create_self_monitoring_project def create_self_monitoring_project
return self_monitoring_project_not_implemented unless Feature.enabled?(:self_monitoring_project)
job_id = SelfMonitoringProjectCreateWorker.perform_async job_id = SelfMonitoringProjectCreateWorker.perform_async
render status: :accepted, json: { render status: :accepted, json: {
...@@ -85,8 +89,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -85,8 +89,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end end
def status_create_self_monitoring_project def status_create_self_monitoring_project
return self_monitoring_project_not_implemented unless Feature.enabled?(:self_monitoring_project)
job_id = params[:job_id].to_s job_id = params[:job_id].to_s
unless job_id.length <= PARAM_JOB_ID_MAX_SIZE unless job_id.length <= PARAM_JOB_ID_MAX_SIZE
...@@ -97,23 +99,66 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -97,23 +99,66 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end end
if Gitlab::CurrentSettings.instance_administration_project_id.present? if Gitlab::CurrentSettings.instance_administration_project_id.present?
render status: :ok, json: self_monitoring_data return render status: :ok, json: self_monitoring_data
elsif SelfMonitoringProjectCreateWorker.in_progress?(job_id) elsif SelfMonitoringProjectCreateWorker.in_progress?(job_id)
::Gitlab::PollingInterval.set_header(response, interval: 3_000) ::Gitlab::PollingInterval.set_header(response, interval: 3_000)
render status: :accepted, json: { message: _('Job is in progress') } return render status: :accepted, json: {
message: _('Job to create self-monitoring project is in progress')
}
end
render status: :bad_request, json: {
message: _('Self-monitoring project does not exist. Please check logs ' \
'for any error messages')
}
end
def delete_self_monitoring_project
job_id = SelfMonitoringProjectDeleteWorker.perform_async
render status: :accepted, json: {
job_id: job_id,
monitor_status: status_delete_self_monitoring_project_admin_application_settings_path
}
end
def status_delete_self_monitoring_project
job_id = params[:job_id].to_s
unless job_id.length <= PARAM_JOB_ID_MAX_SIZE
return render status: :bad_request, json: {
message: _('Parameter "job_id" cannot exceed length of %{job_id_max_size}' %
{ job_id_max_size: PARAM_JOB_ID_MAX_SIZE })
}
end
else if Gitlab::CurrentSettings.instance_administration_project_id.nil?
render status: :bad_request, json: { return render status: :ok, json: {
message: _('Self-monitoring project does not exist. Please check logs ' \ message: _('Self-monitoring project has been successfully deleted')
'for any error messages') }
elsif SelfMonitoringProjectDeleteWorker.in_progress?(job_id)
::Gitlab::PollingInterval.set_header(response, interval: 3_000)
return render status: :accepted, json: {
message: _('Job to delete self-monitoring project is in progress')
} }
end end
render status: :bad_request, json: {
message: _('Self-monitoring project was not deleted. Please check logs ' \
'for any error messages')
}
end end
private private
def validate_self_monitoring_feature_flag_enabled
self_monitoring_project_not_implemented unless Feature.enabled?(:self_monitoring_project)
end
def self_monitoring_data def self_monitoring_data
{ {
project_id: Gitlab::CurrentSettings.instance_administration_project_id, project_id: Gitlab::CurrentSettings.instance_administration_project_id,
......
...@@ -18,7 +18,7 @@ module RequiresWhitelistedMonitoringClient ...@@ -18,7 +18,7 @@ module RequiresWhitelistedMonitoringClient
# debugging purposes # debugging purposes
return true if Rails.env.development? && request.local? return true if Rails.env.development? && request.local?
ip_whitelist.any? { |e| e.include?(Gitlab::RequestContext.client_ip) } ip_whitelist.any? { |e| e.include?(Gitlab::RequestContext.instance.client_ip) }
end end
def ip_whitelist def ip_whitelist
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
class Groups::GroupLinksController < Groups::ApplicationController class Groups::GroupLinksController < Groups::ApplicationController
before_action :check_feature_flag! before_action :check_feature_flag!
before_action :authorize_admin_group! before_action :authorize_admin_group!
before_action :group_link, only: [:update, :destroy]
def create def create
shared_with_group = Group.find(params[:shared_with_group_id]) if params[:shared_with_group_id].present? shared_with_group = Group.find(params[:shared_with_group_id]) if params[:shared_with_group_id].present?
...@@ -22,12 +23,35 @@ class Groups::GroupLinksController < Groups::ApplicationController ...@@ -22,12 +23,35 @@ class Groups::GroupLinksController < Groups::ApplicationController
redirect_to group_group_members_path(group) redirect_to group_group_members_path(group)
end end
def update
@group_link.update(group_link_params)
end
def destroy
Groups::GroupLinks::DestroyService.new(nil, nil).execute(@group_link)
respond_to do |format|
format.html do
redirect_to group_group_members_path(group), status: :found
end
format.js { head :ok }
end
end
private private
def group_link
@group_link ||= group.shared_with_group_links.find(params[:id])
end
def group_link_create_params def group_link_create_params
params.permit(:shared_group_access, :expires_at) params.permit(:shared_group_access, :expires_at)
end end
def group_link_params
params.require(:group_link).permit(:group_access, :expires_at)
end
def check_feature_flag! def check_feature_flag!
render_404 unless Feature.enabled?(:share_group_with_group) render_404 unless Feature.enabled?(:share_group_with_group)
end end
......
...@@ -20,28 +20,17 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -20,28 +20,17 @@ class Groups::GroupMembersController < Groups::ApplicationController
:override :override
def index def index
can_manage_members = can?(current_user, :admin_group_member, @group)
@sort = params[:sort].presence || sort_value_name @sort = params[:sort].presence || sort_value_name
@project = @group.projects.find(params[:project_id]) if params[:project_id] @project = @group.projects.find(params[:project_id]) if params[:project_id]
@members = find_members @members = find_members
if can_manage_members if can_manage_members
@invited_members = @members.invite @skip_groups = @group.related_group_ids
@invited_members = @invited_members.search_invite_email(params[:search_invited]) if params[:search_invited].present? @invited_members = present_invited_members(@members)
@invited_members = present_members(@invited_members.page(params[:invited_members_page]).per(MEMBER_PER_PAGE_LIMIT))
end end
@members = @members.non_invite @members = @members.non_invite
@members = @members.search(params[:search]) if params[:search].present? @members = present_group_members(@members)
@members = @members.sort_by_attribute(@sort)
if can_manage_members && params[:two_factor].present?
@members = @members.filter_by_2fa(params[:two_factor])
end
@members = @members.page(params[:page]).per(MEMBER_PER_PAGE_LIMIT)
@members = present_members(@members)
@requesters = present_members( @requesters = present_members(
AccessRequestsFinder.new(@group).execute(current_user)) AccessRequestsFinder.new(@group).execute(current_user))
...@@ -54,8 +43,30 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -54,8 +43,30 @@ class Groups::GroupMembersController < Groups::ApplicationController
private private
def present_invited_members(members)
invited_members = members.invite
if params[:search_invited].present?
invited_members = invited_members.search_invite_email(params[:search_invited])
end
present_members(invited_members
.page(params[:invited_members_page])
.per(MEMBER_PER_PAGE_LIMIT))
end
def find_members def find_members
GroupMembersFinder.new(@group).execute(include_relations: requested_relations) filter_params = params.slice(:two_factor, :search).merge(sort: @sort)
GroupMembersFinder.new(@group, current_user).execute(include_relations: requested_relations, params: filter_params)
end
def can_manage_members
can?(current_user, :admin_group_member, @group)
end
def present_group_members(original_members)
members = original_members.page(params[:page]).per(MEMBER_PER_PAGE_LIMIT)
present_members(members)
end end
end end
......
# frozen_string_literal: true # frozen_string_literal: true
class GroupMembersFinder < UnionFinder class GroupMembersFinder < UnionFinder
def initialize(group) # Params can be any of the following:
# two_factor: string. 'enabled' or 'disabled' are returning different set of data, other values are not effective.
# sort: string
# search: string
def initialize(group, user = nil)
@group = group @group = group
@user = user
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def execute(include_relations: [:inherited, :direct]) def execute(include_relations: [:inherited, :direct], params: {})
group_members = @group.members group_members = group.members
relations = [] relations = []
return group_members if include_relations == [:direct] return group_members if include_relations == [:direct]
relations << group_members if include_relations.include?(:direct) relations << group_members if include_relations.include?(:direct)
if include_relations.include?(:inherited) && @group.parent if include_relations.include?(:inherited) && group.parent
parents_members = GroupMember.non_request parents_members = GroupMember.non_request
.where(source_id: @group.ancestors.select(:id)) .where(source_id: group.ancestors.select(:id))
.where.not(user_id: @group.users.select(:id)) .where.not(user_id: group.users.select(:id))
relations << parents_members relations << parents_members
end end
if include_relations.include?(:descendants) if include_relations.include?(:descendants)
descendant_members = GroupMember.non_request descendant_members = GroupMember.non_request
.where(source_id: @group.descendants.select(:id)) .where(source_id: group.descendants.select(:id))
.where.not(user_id: @group.users.select(:id)) .where.not(user_id: group.users.select(:id))
relations << descendant_members relations << descendant_members
end end
find_union(relations, GroupMember) members = find_union(relations, GroupMember)
filter_members(members, params)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
private
attr_reader :user, :group
def filter_members(members, params)
members = members.search(params[:search]) if params[:search].present?
members = members.sort_by_attribute(params[:sort]) if params[:sort].present?
if can_manage_members && params[:two_factor].present?
members = members.filter_by_2fa(params[:two_factor])
end
members
end
def can_manage_members
Ability.allowed?(user, :admin_group_member, group)
end
end end
GroupMembersFinder.prepend_if_ee('EE::GroupMembersFinder') GroupMembersFinder.prepend_if_ee('EE::GroupMembersFinder')
...@@ -42,7 +42,7 @@ module Mutations ...@@ -42,7 +42,7 @@ module Mutations
if project_path.present? if project_path.present?
project = find_project!(project_path: project_path) project = find_project!(project_path: project_path)
elsif !can_create_personal_snippet? elsif !can_create_personal_snippet?
raise_resource_not_avaiable_error! raise_resource_not_available_error!
end end
snippet = CreateSnippetService.new(project, snippet = CreateSnippetService.new(project,
......
...@@ -344,6 +344,12 @@ module ApplicationSettingsHelper ...@@ -344,6 +344,12 @@ module ApplicationSettingsHelper
'status_create_self_monitoring_project_path' => 'status_create_self_monitoring_project_path' =>
status_create_self_monitoring_project_admin_application_settings_path, status_create_self_monitoring_project_admin_application_settings_path,
'delete_self_monitoring_project_path' =>
delete_self_monitoring_project_admin_application_settings_path,
'status_delete_self_monitoring_project_path' =>
status_delete_self_monitoring_project_admin_application_settings_path,
'self_monitoring_project_exists' => 'self_monitoring_project_exists' =>
Gitlab::CurrentSettings.instance_administration_project.present?, Gitlab::CurrentSettings.instance_administration_project.present?,
......
...@@ -4,6 +4,10 @@ module Groups::GroupMembersHelper ...@@ -4,6 +4,10 @@ module Groups::GroupMembersHelper
def group_member_select_options def group_member_select_options
{ multiple: true, class: 'input-clamp qa-member-select-field ', scope: :all, email_user: true } { multiple: true, class: 'input-clamp qa-member-select-field ', scope: :all, email_user: true }
end end
def render_invite_member_for_group(group, default_access_level)
render 'shared/members/invite_member', submit_url: group_group_members_path(group), access_levels: GroupMember.access_level_roles, default_access_level: default_access_level
end
end end
Groups::GroupMembersHelper.prepend_if_ee('EE::Groups::GroupMembersHelper') Groups::GroupMembersHelper.prepend_if_ee('EE::Groups::GroupMembersHelper')
...@@ -85,7 +85,8 @@ module SelectsHelper ...@@ -85,7 +85,8 @@ module SelectsHelper
first_user: opts[:first_user] && current_user ? current_user.username : false, first_user: opts[:first_user] && current_user ? current_user.username : false,
current_user: opts[:current_user] || false, current_user: opts[:current_user] || false,
author_id: opts[:author_id] || '', author_id: opts[:author_id] || '',
skip_users: opts[:skip_users] ? opts[:skip_users].map(&:id) : nil skip_users: opts[:skip_users] ? opts[:skip_users].map(&:id) : nil,
qa_selector: opts[:qa_selector] || ''
} }
end end
end end
......
...@@ -169,7 +169,11 @@ class ApplicationSetting < ApplicationRecord ...@@ -169,7 +169,11 @@ class ApplicationSetting < ApplicationRecord
validates :gitaly_timeout_default, validates :gitaly_timeout_default,
presence: true, presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 } numericality: {
only_integer: true,
greater_than_or_equal_to: 0,
less_than_or_equal_to: Settings.gitlab.max_request_duration_seconds
}
validates :gitaly_timeout_medium, validates :gitaly_timeout_medium,
presence: true, presence: true,
......
...@@ -420,6 +420,12 @@ class Group < Namespace ...@@ -420,6 +420,12 @@ class Group < Namespace
GroupMember.where(source_id: self_and_ancestors_ids, user_id: user.id).order(:access_level).last GroupMember.where(source_id: self_and_ancestors_ids, user_id: user.id).order(:access_level).last
end end
def related_group_ids
[id,
*ancestors.pluck(:id),
*shared_with_group_links.pluck(:shared_with_group_id)]
end
def hashed_storage?(_feature) def hashed_storage?(_feature)
false false
end end
......
...@@ -20,4 +20,8 @@ class GroupGroupLink < ApplicationRecord ...@@ -20,4 +20,8 @@ class GroupGroupLink < ApplicationRecord
def self.default_access def self.default_access
Gitlab::Access::DEVELOPER Gitlab::Access::DEVELOPER
end end
def human_access
Gitlab::Access.human_access(self.group_access)
end
end end
...@@ -21,6 +21,8 @@ class ProjectGroupLink < ApplicationRecord ...@@ -21,6 +21,8 @@ class ProjectGroupLink < ApplicationRecord
after_commit :refresh_group_members_authorized_projects after_commit :refresh_group_members_authorized_projects
alias_method :shared_with_group, :group
def self.access_options def self.access_options
Gitlab::Access.options Gitlab::Access.options
end end
......
...@@ -8,6 +8,9 @@ ...@@ -8,6 +8,9 @@
.form-text.text-muted .form-text.text-muted
Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced
for git fetch/push operations or Sidekiq jobs. for git fetch/push operations or Sidekiq jobs.
This timeout should be less than the worker timeout. If a Gitaly call timeout would exceed the
worker timeout, the remaining time from the worker timeout would be used to avoid having to terminate
the worker.
.form-group .form-group
= f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'label-bold' = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'label-bold'
= f.number_field :gitaly_timeout_fast, class: 'form-control' = f.number_field :gitaly_timeout_fast, class: 'form-control'
......
= form_for @group_member, url: group_group_members_path(@group), html: { class: 'users-project-form users-group-form' } do |f|
.row
.col-md-4.col-lg-6
= users_select_tag(:user_ids, group_member_select_options)
.form-text.text-muted.append-bottom-10
Search for members by name, username, or email, or invite new ones using their email address.
.col-md-3.col-lg-2
= select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "form-control project-access-select"
.form-text.text-muted.append-bottom-10
= link_to "Read more", help_page_path("user/permissions")
about role permissions
.col-md-3.col-lg-2
.clearable-input
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
%i.clear-icon.js-clear-input
.form-text.text-muted.append-bottom-10
On this date, the member(s) will automatically lose access to this group and all of its projects.
.col-md-2
= f.submit 'Add to group', class: "btn btn-success btn-block", data: { qa_selector: 'add_to_group_button' }
- page_title _("Members") - page_title _("Group members")
- can_manage_members = can?(current_user, :admin_group_member, @group) - can_manage_members = can?(current_user, :admin_group_member, @group)
- show_invited_members = can_manage_members && @invited_members.exists? - show_invited_members = can_manage_members && @invited_members.exists?
- pending_active = params[:search_invited].present? - pending_active = params[:search_invited].present?
- total_count = @members.count + @group.shared_with_group_links.count
.project-members-page.prepend-top-default .project-members-page.prepend-top-default
%h4 %h4
= _("Members") = _("Group members")
%hr %hr
- if can_manage_members - if can_manage_members
.project-members-new.append-bottom-default - if Feature.enabled?(:share_group_with_group)
%p.clearfix %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
= _("Add new member to %{strong_start}%{group_name}%{strong_end}").html_safe % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe } %li.nav-tab{ role: 'presentation' }
= render "new_group_member" %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' }= _("Invite member")
%li.nav-tab{ role: 'presentation' }
%a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab', qa_selector: 'invite_group_tab' }, role: 'tab' }= _("Invite group")
.tab-content.gitlab-tab-content
.tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' }
= render_invite_member_for_group(@group, @group_member.access_level)
- if Feature.enabled?(:share_group_with_group)
.tab-pane{ id: 'invite-group-pane', role: 'tabpanel' }
= render 'shared/members/invite_group', submit_url: group_group_links_path(@group), access_levels: GroupMember.access_level_roles, default_access_level: @group_member.access_level, group_link_field: 'shared_with_group_id', group_access_field: 'shared_group_access'
- else
= render_invite_member_for_group(@group, @group_member.access_level)
= render 'shared/members/requests', membership_source: @group, requesters: @requesters = render 'shared/members/requests', membership_source: @group, requesters: @requesters
...@@ -19,10 +30,10 @@ ...@@ -19,10 +30,10 @@
%ul.nav-links.mobile-separator.nav.nav-tabs.clearfix %ul.nav-links.mobile-separator.nav.nav-tabs.clearfix
%li.nav-item %li.nav-item
= link_to "#existing_members", class: ["nav-link", ("active" unless pending_active)] , 'data-toggle' => 'tab' do = link_to "#existing_shares", class: ["nav-link", ("active" unless pending_active)] , 'data-toggle' => 'tab' do
%span %span
= _("Existing") = _("Existing shares")
%span.badge.badge-pill= @members.total_count %span.badge.badge-pill= total_count
- if show_invited_members - if show_invited_members
%li.nav-item %li.nav-item
= link_to "#invited_members", class: ["nav-link", ("active" if pending_active)], 'data-toggle' => 'tab' do = link_to "#invited_members", class: ["nav-link", ("active" if pending_active)], 'data-toggle' => 'tab' do
...@@ -31,7 +42,16 @@ ...@@ -31,7 +42,16 @@
%span.badge.badge-pill= @invited_members.total_count %span.badge.badge-pill= @invited_members.total_count
.tab-content .tab-content
#existing_members.tab-pane{ :class => ("active" unless pending_active) } #existing_shares.tab-pane{ :class => ("active" unless pending_active) }
- if @group.shared_with_group_links.any?
.card.card-without-border
.d-flex.flex-column.flex-md-row.row-content-block.second-block
%span.flex-grow-1.align-self-md-center.col-form-label
= _("Groups with access to %{strong_start}%{group_name}%{strong_end}").html_safe % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
%ul.content-list.members-list{ data: { qa_selector: "groups_list" } }
- can_admin_member = can?(current_user, :admin_group_member, @group)
- @group.shared_with_group_links.each do |group_link|
= render 'shared/members/group', group_link: group_link, can_admin_member: can_admin_member, group_link_path: group_group_link_path(@group, group_link)
.card.card-without-border .card.card-without-border
.d-flex.flex-column.flex-md-row.row-content-block.second-block .d-flex.flex-column.flex-md-row.row-content-block.second-block
%span.flex-grow-1.align-self-md-center.col-form-label %span.flex-grow-1.align-self-md-center.col-form-label
...@@ -46,7 +66,7 @@ ...@@ -46,7 +66,7 @@
= label_tag '2fa', '2FA', class: 'col-form-label label-bold pr-md-2' = label_tag '2fa', '2FA', class: 'col-form-label label-bold pr-md-2'
= render 'shared/members/filter_2fa_dropdown' = render 'shared/members/filter_2fa_dropdown'
= render 'shared/members/sort_dropdown' = render 'shared/members/sort_dropdown'
%ul.content-list.members-list %ul.content-list.members-list{ data: { qa_selector: "members_list" } }
= render partial: 'shared/members/member', collection: @members, as: :member = render partial: 'shared/members/member', collection: @members, as: :member
= paginate @members, theme: 'gitlab' = paginate @members, theme: 'gitlab'
......
...@@ -3,4 +3,6 @@ ...@@ -3,4 +3,6 @@
= _("Groups with access to <strong>%{project_name}</strong>").html_safe % { project_name: sanitize(@project.name, tags: []) } = _("Groups with access to <strong>%{project_name}</strong>").html_safe % { project_name: sanitize(@project.name, tags: []) }
%span.badge.badge-pill= group_links.size %span.badge.badge-pill= group_links.size
%ul.content-list.members-list %ul.content-list.members-list
= render partial: 'shared/members/group', collection: group_links, as: :group_link - can_admin_member = can?(current_user, :admin_project_member, @project)
- @group_links.each do |group_link|
= render 'shared/members/group', group_link: group_link, can_admin_member: can_admin_member, group_link_path: project_group_link_path(@project, group_link)
...@@ -13,5 +13,5 @@ ...@@ -13,5 +13,5 @@
%button.user-search-btn{ type: "submit", "aria-label" => _("Submit search") } %button.user-search-btn{ type: "submit", "aria-label" => _("Submit search") }
= icon("search") = icon("search")
= render 'shared/members/sort_dropdown' = render 'shared/members/sort_dropdown'
%ul.content-list.members-list.qa-members-list %ul.content-list.members-list{ data: { qa_selector: 'members_list' } }
= render partial: 'shared/members/member', collection: members, as: :member = render partial: 'shared/members/member', collection: members, as: :member
...@@ -23,13 +23,13 @@ ...@@ -23,13 +23,13 @@
.tab-content.gitlab-tab-content .tab-content.gitlab-tab-content
.tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' } .tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' }
= render 'projects/project_members/new_project_member', tab_title: _('Invite member') = render 'shared/members/invite_member', submit_url: project_project_members_path(@project), access_levels: ProjectMember.access_level_roles, default_access_level: @project_member.access_level, can_import_members?: can_import_members?, import_path: import_project_project_members_path(@project)
.tab-pane{ id: 'invite-group-pane', role: 'tabpanel', class: ('active' if membership_locked?) } .tab-pane{ id: 'invite-group-pane', role: 'tabpanel', class: ('active' if membership_locked?) }
= render 'projects/project_members/new_project_group', tab_title: _('Invite group') = render 'shared/members/invite_group', submit_url: project_group_links_path(@project), access_levels: ProjectGroupLink.access_options, default_access_level: ProjectGroupLink.default_access, group_link_field: 'link_group_id', group_access_field: 'link_group_access'
- elsif !membership_locked? - elsif !membership_locked?
.invite-member= render 'projects/project_members/new_project_member', tab_title: _('Invite member') .invite-member= render 'shared/members/invite_member', submit_url: project_project_members_path(@project), access_levels: ProjectMember.access_level_roles, default_access_level: @project_member.access_level, can_import_members?: can_import_members?, import_path: import_project_project_members_path(@project)
- elsif @project.allowed_to_share_with_group? - elsif @project.allowed_to_share_with_group?
.invite-group= render 'projects/project_members/new_project_group', tab_title: _('Invite group') .invite-group= render 'shared/members/invite_group', access_levels: ProjectGroupLink.access_options, default_access_level: ProjectGroupLink.default_access, submit_url: project_group_links_path(@project), group_link_field: 'link_group_id', group_access_field: 'link_group_access'
= render 'shared/members/requests', membership_source: @project, requesters: @requesters = render 'shared/members/requests', membership_source: @project, requesters: @requesters
.clearfix .clearfix
......
#js-registry-settings{ data: { project_id: @project.id, } } #js-registry-settings{ data: { project_id: @project.id,
cadence_options: cadence_options.to_json,
keep_n_options: keep_n_options.to_json,
older_than_options: older_than_options.to_json} }
- group_link = local_assigns[:group_link] - group_link = local_assigns[:group_link]
- group = group_link.group - group = group_link.shared_with_group
- can_admin_member = can?(current_user, :admin_project_member, @project) - can_admin_member = local_assigns[:can_admin_member]
- group_link_path = local_assigns[:group_link_path]
- dom_id = "group_member_#{group_link.id}" - dom_id = "group_member_#{group_link.id}"
-# Note this is just for groups. For individual members please see shared/members/_member -# Note this is just for groups. For individual members please see shared/members/_member
...@@ -17,7 +18,7 @@ ...@@ -17,7 +18,7 @@
%span{ class: ('text-warning' if group_link.expires_soon?) } %span{ class: ('text-warning' if group_link.expires_soon?) }
= _("Expires in %{expires_at}").html_safe % { expires_at: distance_of_time_in_words_to_now(group_link.expires_at) } = _("Expires in %{expires_at}").html_safe % { expires_at: distance_of_time_in_words_to_now(group_link.expires_at) }
.controls.member-controls.align-items-center .controls.member-controls.align-items-center
= form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form form-group d-sm-flex' do = form_tag group_link_path, method: :put, remote: true, class: 'js-edit-member-form form-group d-sm-flex' do
= hidden_field_tag "group_link[group_access]", group_link.group_access = hidden_field_tag "group_link[group_access]", group_link.group_access
.member-form-control.dropdown.mr-sm-2.d-sm-inline-block .member-form-control.dropdown.mr-sm-2.d-sm-inline-block
%button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button", %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
...@@ -39,7 +40,7 @@ ...@@ -39,7 +40,7 @@
= text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: _('Expiration date'), id: "member_expires_at_#{group.id}", disabled: !can_admin_member = text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: _('Expiration date'), id: "member_expires_at_#{group.id}", disabled: !can_admin_member
%i.clear-icon.js-clear-input %i.clear-icon.js-clear-input
- if can_admin_member - if can_admin_member
= link_to project_group_link_path(@project, group_link), = link_to group_link_path,
method: :delete, method: :delete,
data: { confirm: _("Are you sure you want to remove %{group_name}?") % { group_name: group.name }, qa_selector: 'delete_group_access_link' }, data: { confirm: _("Are you sure you want to remove %{group_name}?") % { group_name: group.name }, qa_selector: 'delete_group_access_link' },
class: 'btn btn-remove m-0 ml-sm-2 align-self-center' do class: 'btn btn-remove m-0 ml-sm-2 align-self-center' do
......
- access_levels = local_assigns[:access_levels]
- default_access_level = local_assigns[:default_access_level]
- submit_url = local_assigns[:submit_url]
- group_link_field = local_assigns[:group_link_field]
- group_access_field = local_assigns[:group_access_field]
.row .row
.col-sm-12 .col-sm-12
= form_tag project_group_links_path(@project), class: 'js-requires-input', method: :post do = form_tag submit_url, class: 'invite-group-form js-requires-input', method: :post do
.form-group .form-group
= label_tag :link_group_id, _("Select a group to invite"), class: "label-bold" = label_tag group_link_field, _("Select a group to invite"), class: "label-bold"
= groups_select_tag(:link_group_id, data: { skip_groups: @skip_groups }, class: "input-clamp qa-group-select-field", required: true) = groups_select_tag(group_link_field, data: { skip_groups: @skip_groups }, class: 'input-clamp qa-group-select-field', required: true)
.form-group .form-group
= label_tag :link_group_access, _("Max access level"), class: "label-bold" = label_tag group_access_field, _("Max access level"), class: "label-bold"
.select-wrapper .select-wrapper
= select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control" = select_tag group_access_field, options_for_select(access_levels, default_access_level), data: { qa_selector: 'group_access_field' }, class: "form-control select-control"
= icon('chevron-down') = icon('chevron-down')
.form-text.text-muted.append-bottom-10 .form-text.text-muted.append-bottom-10
- permissions_docs_path = help_page_path('user/permissions') - permissions_docs_path = help_page_path('user/permissions')
......
- access_levels = local_assigns[:access_levels]
- default_access_level = local_assigns[:default_access_level]
- submit_url = local_assigns[:submit_url]
- can_import_members = local_assigns[:can_import_members?]
- import_path = local_assigns[:import_path]
.row .row
.col-sm-12 .col-sm-12
= form_for @project_member, as: :project_member, url: project_project_members_path(@project), html: { class: 'users-project-form' } do |f| = form_tag submit_url, class: 'invite-users-form', method: :post do
.form-group .form-group
= label_tag :user_ids, _("GitLab member or Email address"), class: "label-bold" = label_tag :user_ids, _("GitLab member or Email address"), class: "label-bold"
= users_select_tag(:user_ids, multiple: true, class: "input-clamp qa-member-select-input", scope: :all, email_user: true, placeholder: "Search for members to update or invite") = users_select_tag(:user_ids, multiple: true, class: 'input-clamp qa-member-select-field', scope: :all, email_user: true, placeholder: 'Search for members to update or invite')
.form-group .form-group
= label_tag :access_level, _("Choose a role permission"), class: "label-bold" = label_tag :access_level, _("Choose a role permission"), class: "label-bold"
.select-wrapper .select-wrapper
= select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select select-control" = select_tag :access_level, options_for_select(access_levels, default_access_level), class: "form-control project-access-select select-control"
= icon('chevron-down') = icon('chevron-down')
.form-text.text-muted.append-bottom-10 .form-text.text-muted.append-bottom-10
- permissions_docs_path = help_page_path('user/permissions') - permissions_docs_path = help_page_path('user/permissions')
...@@ -18,6 +23,6 @@ ...@@ -18,6 +23,6 @@
= label_tag :expires_at, _('Access expiration date'), class: 'label-bold' = label_tag :expires_at, _('Access expiration date'), class: 'label-bold'
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
%i.clear-icon.js-clear-input %i.clear-icon.js-clear-input
= f.submit _("Add to project"), class: "btn btn-success qa-add-member-button" = submit_tag _("Invite"), class: "btn btn-success", data: { qa_selector: 'invite_member_button' }
- if can_import_members? - if can_import_members
= link_to _("Import"), import_project_project_members_path(@project), class: "btn btn-default", title: _("Import members from another project") = link_to _("Import"), import_path, class: "btn btn-default", title: _("Import members from another project")
---
title: Accept `Envelope-To` as possible location for Service Desk key
merge_request: 22354
author: Max Winterstein
type: added
---
title: Don't let Gitaly calls exceed a request time of 55 seconds
merge_request: 21492
author:
type: performance
...@@ -33,6 +33,9 @@ production: &base ...@@ -33,6 +33,9 @@ production: &base
host: localhost host: localhost
port: 80 # Set to 443 if using HTTPS, see installation.md#using-https for additional HTTPS configuration details port: 80 # Set to 443 if using HTTPS, see installation.md#using-https for additional HTTPS configuration details
https: false # Set to true if using HTTPS, see installation.md#using-https for additional HTTPS configuration details https: false # Set to true if using HTTPS, see installation.md#using-https for additional HTTPS configuration details
# The maximum time unicorn/puma can spend on the request. This needs to be smaller than the worker timeout.
# Default is 95% of the worker timeout
max_request_duration: 57
# Uncomment this line below if your ssh host is different from HTTP/HTTPS one # Uncomment this line below if your ssh host is different from HTTP/HTTPS one
# (you'd obviously need to replace ssh.host_example.com with your own host). # (you'd obviously need to replace ssh.host_example.com with your own host).
......
...@@ -209,6 +209,7 @@ Settings.gitlab['content_security_policy'] ||= Gitlab::ContentSecurityPolicy::Co ...@@ -209,6 +209,7 @@ Settings.gitlab['content_security_policy'] ||= Gitlab::ContentSecurityPolicy::Co
Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml')) Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml'))
Settings.gitlab['impersonation_enabled'] ||= true if Settings.gitlab['impersonation_enabled'].nil? Settings.gitlab['impersonation_enabled'] ||= true if Settings.gitlab['impersonation_enabled'].nil?
Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil? Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil?
Settings.gitlab['max_request_duration_seconds'] ||= 57
Gitlab.ee do Gitlab.ee do
Settings.gitlab['mirror_max_delay'] ||= 300 Settings.gitlab['mirror_max_delay'] ||= 300
......
...@@ -37,7 +37,7 @@ unless Gitlab::Runtime.sidekiq? ...@@ -37,7 +37,7 @@ unless Gitlab::Runtime.sidekiq?
payload[:response] = event.payload[:response] if event.payload[:response] payload[:response] = event.payload[:response] if event.payload[:response]
payload[Labkit::Correlation::CorrelationId::LOG_KEY] = Labkit::Correlation::CorrelationId.current_id payload[Labkit::Correlation::CorrelationId::LOG_KEY] = Labkit::Correlation::CorrelationId.current_id
if cpu_s = Gitlab::Metrics::System.thread_cpu_duration(::Gitlab::RequestContext.start_thread_cpu_time) if cpu_s = Gitlab::Metrics::System.thread_cpu_duration(::Gitlab::RequestContext.instance.start_thread_cpu_time)
payload[:cpu_s] = cpu_s payload[:cpu_s] = cpu_s
end end
......
Rails.application.configure do |config| Rails.application.configure do |config|
config.middleware.insert_after RequestStore::Middleware, Gitlab::RequestContext config.middleware.insert_after RequestStore::Middleware, Gitlab::Middleware::RequestContext
end end
...@@ -119,6 +119,8 @@ namespace :admin do ...@@ -119,6 +119,8 @@ namespace :admin do
post :create_self_monitoring_project post :create_self_monitoring_project
get :status_create_self_monitoring_project get :status_create_self_monitoring_project
delete :delete_self_monitoring_project
get :status_delete_self_monitoring_project
end end
resources :labels resources :labels
......
# Uploads Migrate Rake Task # Uploads Migrate Rake Tasks
## Migrate to Object Storage ## Migrate to Object Storage
...@@ -110,7 +110,15 @@ sudo -u git -H bundle exec rake "gitlab:uploads:migrate[FileUploader, MergeReque ...@@ -110,7 +110,15 @@ sudo -u git -H bundle exec rake "gitlab:uploads:migrate[FileUploader, MergeReque
To migrate all uploads created by legacy uploaders, run: To migrate all uploads created by legacy uploaders, run:
```shell **Omnibus Installation**
```bash
gitlab-rake gitlab:uploads:legacy:migrate
```
**Source Installation**
```bash
bundle exec rake gitlab:uploads:legacy:migrate bundle exec rake gitlab:uploads:legacy:migrate
``` ```
......
...@@ -14,5 +14,5 @@ comments: false ...@@ -14,5 +14,5 @@ comments: false
- [Webhooks](web_hooks.md) - [Webhooks](web_hooks.md)
- [Import](import.md) of Git repositories in bulk - [Import](import.md) of Git repositories in bulk
- [Rebuild authorized_keys file](../administration/raketasks/maintenance.md#rebuild-authorized_keys-file) task for administrators - [Rebuild authorized_keys file](../administration/raketasks/maintenance.md#rebuild-authorized_keys-file) task for administrators
- [Migrate Uploads](../administration/raketasks/uploads/migrate.md) - [Uploads Migrate](../administration/raketasks/uploads/migrate.md)
- [Sanitize Uploads](../administration/raketasks/uploads/sanitize.md) - [Uploads Sanitize](../administration/raketasks/uploads/sanitize.md)
...@@ -134,8 +134,8 @@ Please follow the [Upgrade Recommendations](../policy/maintenance.md#upgrade-rec ...@@ -134,8 +134,8 @@ Please follow the [Upgrade Recommendations](../policy/maintenance.md#upgrade-rec
to identify the ideal upgrade path. to identify the ideal upgrade path.
Before upgrading to a new major version, you should ensure that any background Before upgrading to a new major version, you should ensure that any background
migration jobs from previous releases have been completed. The number of remaining migration jobs from previous releases have been completed. To see the current size
migrations jobs can be found by running the following command: of the `background_migration` queue, [check for background migrations before upgrading](#checking-for-background-migrations-before-upgrading).
## Upgrading between editions ## Upgrading between editions
......
...@@ -8,7 +8,7 @@ module Gitlab ...@@ -8,7 +8,7 @@ module Gitlab
class << self class << self
def limit_user_id!(user_id) def limit_user_id!(user_id)
if config.unique_ips_limit_enabled if config.unique_ips_limit_enabled
ip = RequestContext.client_ip ip = RequestContext.instance.client_ip
unique_ips = update_and_return_ips_count(user_id, ip) unique_ips = update_and_return_ips_count(user_id, ip)
raise TooManyIps.new(user_id, ip, unique_ips) if unique_ips > config.unique_ips_limit_per_user raise TooManyIps.new(user_id, ip, unique_ips) if unique_ips > config.unique_ips_limit_per_user
......
...@@ -66,7 +66,8 @@ module Gitlab ...@@ -66,7 +66,8 @@ module Gitlab
def key_from_additional_headers(mail) def key_from_additional_headers(mail)
find_key_from_references(mail) || find_key_from_references(mail) ||
find_key_from_delivered_to_header(mail) find_key_from_delivered_to_header(mail) ||
find_key_from_envelope_to_header(mail)
end end
def ensure_references_array(references) def ensure_references_array(references)
...@@ -96,6 +97,13 @@ module Gitlab ...@@ -96,6 +97,13 @@ module Gitlab
end end
end end
def find_key_from_envelope_to_header(mail)
Array(mail[:envelope_to]).find do |header|
key = Gitlab::IncomingEmail.key_from_address(header.value)
break key if key
end
end
def ignore_auto_reply!(mail) def ignore_auto_reply!(mail)
if auto_submitted?(mail) || auto_replied?(mail) if auto_submitted?(mail) || auto_replied?(mail)
raise AutoGeneratedEmailError raise AutoGeneratedEmailError
......
...@@ -160,6 +160,7 @@ module Gitlab ...@@ -160,6 +160,7 @@ module Gitlab
def self.execute(storage, service, rpc, request, remote_storage:, timeout:) def self.execute(storage, service, rpc, request, remote_storage:, timeout:)
enforce_gitaly_request_limits(:call) enforce_gitaly_request_limits(:call)
Gitlab::RequestContext.instance.ensure_deadline_not_exceeded!
kwargs = request_kwargs(storage, timeout: timeout.to_f, remote_storage: remote_storage) kwargs = request_kwargs(storage, timeout: timeout.to_f, remote_storage: remote_storage)
kwargs = yield(kwargs) if block_given? kwargs = yield(kwargs) if block_given?
...@@ -234,12 +235,28 @@ module Gitlab ...@@ -234,12 +235,28 @@ module Gitlab
metadata['gitaly-session-id'] = session_id metadata['gitaly-session-id'] = session_id
metadata.merge!(Feature::Gitaly.server_feature_flags) metadata.merge!(Feature::Gitaly.server_feature_flags)
result = { metadata: metadata } deadline_info = request_deadline(timeout)
metadata.merge!(deadline_info.slice(:deadline_type))
result[:deadline] = real_time + timeout if timeout > 0 { metadata: metadata, deadline: deadline_info[:deadline] }
result
end end
def self.request_deadline(timeout)
# timeout being 0 means the request is allowed to run indefinitely.
# We can't allow that inside a request, but this won't count towards Gitaly
# error budgets
regular_deadline = real_time.to_i + timeout if timeout > 0
return { deadline: regular_deadline } if Sidekiq.server?
return { deadline: regular_deadline } unless Gitlab::RequestContext.instance.request_deadline
limited_deadline = [regular_deadline, Gitlab::RequestContext.instance.request_deadline].compact.min
limited = limited_deadline < regular_deadline
{ deadline: limited_deadline, deadline_type: limited ? "limited" : "regular" }
end
private_class_method :request_deadline
def self.session_id def self.session_id
Gitlab::SafeRequestStore[:gitaly_session_id] ||= SecureRandom.uuid Gitlab::SafeRequestStore[:gitaly_session_id] ||= SecureRandom.uuid
end end
......
...@@ -40,7 +40,7 @@ module Gitlab ...@@ -40,7 +40,7 @@ module Gitlab
def authorize!(object) def authorize!(object)
unless authorized_resource?(object) unless authorized_resource?(object)
raise_resource_not_avaiable_error! raise_resource_not_available_error!
end end
end end
...@@ -63,7 +63,7 @@ module Gitlab ...@@ -63,7 +63,7 @@ module Gitlab
end end
end end
def raise_resource_not_avaiable_error! def raise_resource_not_available_error!
raise Gitlab::Graphql::Errors::ResourceNotAvailable, RESOURCE_ACCESS_ERROR raise Gitlab::Graphql::Errors::ResourceNotAvailable, RESOURCE_ACCESS_ERROR
end end
end end
......
# frozen_string_literal: true
module Gitlab
module Middleware
class RequestContext
def initialize(app)
@app = app
end
def call(env)
# We should be using ActionDispatch::Request instead of
# Rack::Request to be consistent with Rails, but due to a Rails
# bug described in
# https://gitlab.com/gitlab-org/gitlab-foss/issues/58573#note_149799010
# hosts behind a load balancer will only see 127.0.0.1 for the
# load balancer's IP.
req = Rack::Request.new(env)
Gitlab::RequestContext.instance.client_ip = req.ip
Gitlab::RequestContext.instance.start_thread_cpu_time = Gitlab::Metrics::System.thread_cpu_time
Gitlab::RequestContext.instance.request_start_time = Gitlab::Metrics::System.real_time
@app.call(env)
end
end
end
end
...@@ -2,34 +2,37 @@ ...@@ -2,34 +2,37 @@
module Gitlab module Gitlab
class RequestContext class RequestContext
class << self include Singleton
def client_ip
Gitlab::SafeRequestStore[:client_ip] RequestDeadlineExceeded = Class.new(StandardError)
end
attr_accessor :client_ip, :start_thread_cpu_time, :request_start_time
def start_thread_cpu_time class << self
Gitlab::SafeRequestStore[:start_thread_cpu_time] def instance
Gitlab::SafeRequestStore[:request_context] ||= new
end end
end end
def initialize(app) def request_deadline
@app = app return unless request_start_time
return unless Feature.enabled?(:request_deadline)
@request_deadline ||= request_start_time + max_request_duration_seconds
end end
def call(env) def ensure_deadline_not_exceeded!
# We should be using ActionDispatch::Request instead of return unless request_deadline
# Rack::Request to be consistent with Rails, but due to a Rails return if Gitlab::Metrics::System.real_time < request_deadline
# bug described in
# https://gitlab.com/gitlab-org/gitlab-foss/issues/58573#note_149799010
# hosts behind a load balancer will only see 127.0.0.1 for the
# load balancer's IP.
req = Rack::Request.new(env)
Gitlab::SafeRequestStore[:client_ip] = req.ip raise RequestDeadlineExceeded,
"Request takes longer than #{max_request_duration_seconds}"
end
Gitlab::SafeRequestStore[:start_thread_cpu_time] = Gitlab::Metrics::System.thread_cpu_time private
@app.call(env) def max_request_duration_seconds
Settings.gitlab.max_request_duration_seconds
end end
end end
end end
...@@ -1068,9 +1068,6 @@ msgstr "" ...@@ -1068,9 +1068,6 @@ msgstr ""
msgid "Add new directory" msgid "Add new directory"
msgstr "" msgstr ""
msgid "Add new member to %{strong_start}%{group_name}%{strong_end}"
msgstr ""
msgid "Add or subtract spent time" msgid "Add or subtract spent time"
msgstr "" msgstr ""
...@@ -1095,9 +1092,6 @@ msgstr "" ...@@ -1095,9 +1092,6 @@ msgstr ""
msgid "Add to merge train when pipeline succeeds" msgid "Add to merge train when pipeline succeeds"
msgstr "" msgstr ""
msgid "Add to project"
msgstr ""
msgid "Add to review" msgid "Add to review"
msgstr "" msgstr ""
...@@ -7458,10 +7452,10 @@ msgstr "" ...@@ -7458,10 +7452,10 @@ msgstr ""
msgid "Excluding merge commits. Limited to 6,000 commits." msgid "Excluding merge commits. Limited to 6,000 commits."
msgstr "" msgstr ""
msgid "Existing" msgid "Existing members and groups"
msgstr "" msgstr ""
msgid "Existing members and groups" msgid "Existing shares"
msgstr "" msgstr ""
msgid "Expand" msgid "Expand"
...@@ -9122,6 +9116,9 @@ msgstr "" ...@@ -9122,6 +9116,9 @@ msgstr ""
msgid "Group maintainers can register group runners in the %{link}" msgid "Group maintainers can register group runners in the %{link}"
msgstr "" msgstr ""
msgid "Group members"
msgstr ""
msgid "Group name" msgid "Group name"
msgstr "" msgstr ""
...@@ -9392,6 +9389,9 @@ msgstr "" ...@@ -9392,6 +9389,9 @@ msgstr ""
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}." msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr "" msgstr ""
msgid "Groups with access to %{strong_start}%{group_name}%{strong_end}"
msgstr ""
msgid "Groups with access to <strong>%{project_name}</strong>" msgid "Groups with access to <strong>%{project_name}</strong>"
msgstr "" msgstr ""
...@@ -10314,9 +10314,6 @@ msgstr "" ...@@ -10314,9 +10314,6 @@ msgstr ""
msgid "Job has wrong arguments format." msgid "Job has wrong arguments format."
msgstr "" msgstr ""
msgid "Job is in progress"
msgstr ""
msgid "Job is missing the `model_type` argument." msgid "Job is missing the `model_type` argument."
msgstr "" msgstr ""
...@@ -10326,6 +10323,12 @@ msgstr "" ...@@ -10326,6 +10323,12 @@ msgstr ""
msgid "Job logs and artifacts" msgid "Job logs and artifacts"
msgstr "" msgstr ""
msgid "Job to create self-monitoring project is in progress"
msgstr ""
msgid "Job to delete self-monitoring project is in progress"
msgstr ""
msgid "Job was retried" msgid "Job was retried"
msgstr "" msgstr ""
...@@ -12639,12 +12642,6 @@ msgstr "" ...@@ -12639,12 +12642,6 @@ msgstr ""
msgid "OperationsDashboard|The operations dashboard provides a summary of each project's operational health, including pipeline and alert statuses." msgid "OperationsDashboard|The operations dashboard provides a summary of each project's operational health, including pipeline and alert statuses."
msgstr "" msgstr ""
msgid "Option 1"
msgstr ""
msgid "Option 2"
msgstr ""
msgid "Optional" msgid "Optional"
msgstr "" msgstr ""
...@@ -15784,9 +15781,6 @@ msgstr "" ...@@ -15784,9 +15781,6 @@ msgstr ""
msgid "Save Changes" msgid "Save Changes"
msgstr "" msgstr ""
msgid "Save Expiration Policy"
msgstr ""
msgid "Save anyway" msgid "Save anyway"
msgstr "" msgstr ""
...@@ -15802,6 +15796,9 @@ msgstr "" ...@@ -15802,6 +15796,9 @@ msgstr ""
msgid "Save comment" msgid "Save comment"
msgstr "" msgstr ""
msgid "Save expiration policy"
msgstr ""
msgid "Save password" msgid "Save password"
msgstr "" msgstr ""
...@@ -16373,6 +16370,12 @@ msgstr "" ...@@ -16373,6 +16370,12 @@ msgstr ""
msgid "Self-monitoring project does not exist. Please check logs for any error messages" msgid "Self-monitoring project does not exist. Please check logs for any error messages"
msgstr "" msgstr ""
msgid "Self-monitoring project has been successfully deleted"
msgstr ""
msgid "Self-monitoring project was not deleted. Please check logs for any error messages"
msgstr ""
msgid "Send a separate email notification to Developers." msgid "Send a separate email notification to Developers."
msgstr "" msgstr ""
......
...@@ -7,12 +7,9 @@ module QA ...@@ -7,12 +7,9 @@ module QA
class Members < Page::Base class Members < Page::Base
include Page::Component::UsersSelect include Page::Component::UsersSelect
view 'app/views/groups/group_members/_new_group_member.html.haml' do view 'app/views/shared/members/_invite_member.html.haml' do
element :add_to_group_button
end
view 'app/helpers/groups/group_members_helper.rb' do
element :member_select_field element :member_select_field
element :invite_member_button
end end
view 'app/views/shared/members/_member.html.haml' do view 'app/views/shared/members/_member.html.haml' do
...@@ -24,7 +21,7 @@ module QA ...@@ -24,7 +21,7 @@ module QA
def add_member(username) def add_member(username)
select_user :member_select_field, username select_user :member_select_field, username
click_element :add_to_group_button click_element :invite_member_button
end end
def update_access_level(username, access_level) def update_access_level(username, access_level)
......
...@@ -8,9 +8,9 @@ module QA ...@@ -8,9 +8,9 @@ module QA
include Page::Component::UsersSelect include Page::Component::UsersSelect
include QA::Page::Component::Select2 include QA::Page::Component::Select2
view 'app/views/projects/project_members/_new_project_member.html.haml' do view 'app/views/shared/members/_invite_member.html.haml' do
element :member_select_input element :member_select_field
element :add_member_button element :invite_member_button
end end
view 'app/views/projects/project_members/_team.html.haml' do view 'app/views/projects/project_members/_team.html.haml' do
...@@ -21,7 +21,7 @@ module QA ...@@ -21,7 +21,7 @@ module QA
element :invite_group_tab element :invite_group_tab
end end
view 'app/views/projects/project_members/_new_project_group.html.haml' do view 'app/views/shared/members/_invite_group.html.haml' do
element :group_select_field element :group_select_field
element :invite_group_button element :invite_group_button
end end
...@@ -43,8 +43,8 @@ module QA ...@@ -43,8 +43,8 @@ module QA
end end
def add_member(username) def add_member(username)
select_user :member_select_input, username select_user :member_select_field, username
click_element :add_member_button click_element :invite_member_button
end end
def remove_group(group_name) def remove_group(group_name)
......
...@@ -111,4 +111,100 @@ describe Groups::GroupLinksController do ...@@ -111,4 +111,100 @@ describe Groups::GroupLinksController do
end end
end end
end end
describe '#update' do
let!(:link) do
create(:group_group_link, { shared_group: shared_group,
shared_with_group: shared_with_group })
end
let(:expiry_date) { 1.month.from_now.to_date }
subject do
post(:update, params: { group_id: shared_group,
id: link.id,
group_link: { group_access: Gitlab::Access::GUEST,
expires_at: expiry_date } })
end
context 'when user has admin access to the shared group' do
before do
shared_group.add_owner(user)
end
it 'updates existing link' do
expect(link.group_access).to eq(Gitlab::Access::DEVELOPER)
expect(link.expires_at).to be_nil
subject
link.reload
expect(link.group_access).to eq(Gitlab::Access::GUEST)
expect(link.expires_at).to eq(expiry_date)
end
end
context 'when user does not have admin access to the shared group' do
it 'renders 404' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(share_group_with_group: false)
end
it 'renders 404' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
end
describe '#destroy' do
let!(:link) do
create(:group_group_link, { shared_group: shared_group,
shared_with_group: shared_with_group })
end
subject do
post(:destroy, params: { group_id: shared_group,
id: link.id })
end
context 'when user has admin access to the shared group' do
before do
shared_group.add_owner(user)
end
it 'deletes existing link' do
expect { subject }.to change(GroupGroupLink, :count).by(-1)
end
end
context 'when user does not have admin access to the shared group' do
it 'renders 404' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(share_group_with_group: false)
end
it 'renders 404' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
end
end end
...@@ -31,6 +31,12 @@ describe Groups::GroupMembersController do ...@@ -31,6 +31,12 @@ describe Groups::GroupMembersController do
expect(assigns(:invited_members).map(&:invite_email)).to match_array(invited.map(&:invite_email)) expect(assigns(:invited_members).map(&:invite_email)).to match_array(invited.map(&:invite_email))
end end
it 'assigns skip groups' do
get :index, params: { group_id: group }
expect(assigns(:skip_groups)).to match_array(group.related_group_ids)
end
it 'restricts search to one email' do it 'restricts search to one email' do
get :index, params: { group_id: group, search_invited: invited.first.invite_email } get :index, params: { group_id: group, search_invited: invited.first.invite_email }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe HealthCheckController do describe HealthCheckController, :request_store do
include StubENV include StubENV
let(:xml_response) { Hash.from_xml(response.body)['hash'] } let(:xml_response) { Hash.from_xml(response.body)['hash'] }
...@@ -18,7 +18,7 @@ describe HealthCheckController do ...@@ -18,7 +18,7 @@ describe HealthCheckController do
describe 'GET #index' do describe 'GET #index' do
context 'when services are up but accessed from outside whitelisted ips' do context 'when services are up but accessed from outside whitelisted ips' do
before do before do
allow(Gitlab::RequestContext).to receive(:client_ip).and_return(not_whitelisted_ip) allow(Gitlab::RequestContext.instance).to receive(:client_ip).and_return(not_whitelisted_ip)
end end
it 'returns a not found page' do it 'returns a not found page' do
...@@ -48,7 +48,7 @@ describe HealthCheckController do ...@@ -48,7 +48,7 @@ describe HealthCheckController do
context 'when services are up and accessed from whitelisted ips' do context 'when services are up and accessed from whitelisted ips' do
before do before do
allow(Gitlab::RequestContext).to receive(:client_ip).and_return(whitelisted_ip) allow(Gitlab::RequestContext.instance).to receive(:client_ip).and_return(whitelisted_ip)
end end
it 'supports successful plaintext response' do it 'supports successful plaintext response' do
...@@ -95,7 +95,7 @@ describe HealthCheckController do ...@@ -95,7 +95,7 @@ describe HealthCheckController do
before do before do
allow(HealthCheck::Utils).to receive(:process_checks).with(['standard']).and_return('The server is on fire') allow(HealthCheck::Utils).to receive(:process_checks).with(['standard']).and_return('The server is on fire')
allow(HealthCheck::Utils).to receive(:process_checks).with(['email']).and_return('Email is on fire') allow(HealthCheck::Utils).to receive(:process_checks).with(['email']).and_return('Email is on fire')
allow(Gitlab::RequestContext).to receive(:client_ip).and_return(whitelisted_ip) allow(Gitlab::RequestContext.instance).to receive(:client_ip).and_return(whitelisted_ip)
end end
it 'supports failure plaintext response' do it 'supports failure plaintext response' do
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe MetricsController do describe MetricsController, :request_store do
include StubENV include StubENV
let(:metrics_multiproc_dir) { @metrics_multiproc_dir } let(:metrics_multiproc_dir) { @metrics_multiproc_dir }
...@@ -53,7 +53,7 @@ describe MetricsController do ...@@ -53,7 +53,7 @@ describe MetricsController do
context 'accessed from whitelisted ip' do context 'accessed from whitelisted ip' do
before do before do
allow(Gitlab::RequestContext).to receive(:client_ip).and_return(whitelisted_ip) allow(Gitlab::RequestContext.instance).to receive(:client_ip).and_return(whitelisted_ip)
end end
it_behaves_like 'endpoint providing metrics' it_behaves_like 'endpoint providing metrics'
...@@ -61,7 +61,7 @@ describe MetricsController do ...@@ -61,7 +61,7 @@ describe MetricsController do
context 'accessed from ip in whitelisted range' do context 'accessed from ip in whitelisted range' do
before do before do
allow(Gitlab::RequestContext).to receive(:client_ip).and_return(ip_in_whitelisted_range) allow(Gitlab::RequestContext.instance).to receive(:client_ip).and_return(ip_in_whitelisted_range)
end end
it_behaves_like 'endpoint providing metrics' it_behaves_like 'endpoint providing metrics'
...@@ -69,7 +69,7 @@ describe MetricsController do ...@@ -69,7 +69,7 @@ describe MetricsController do
context 'accessed from not whitelisted ip' do context 'accessed from not whitelisted ip' do
before do before do
allow(Gitlab::RequestContext).to receive(:client_ip).and_return(not_whitelisted_ip) allow(Gitlab::RequestContext.instance).to receive(:client_ip).and_return(not_whitelisted_ip)
end end
it 'returns the expected error response' do it 'returns the expected error response' do
......
...@@ -167,14 +167,14 @@ describe 'Admin Groups' do ...@@ -167,14 +167,14 @@ describe 'Admin Groups' do
it 'adds admin a to a group as developer', :js do it 'adds admin a to a group as developer', :js do
visit group_group_members_path(group) visit group_group_members_path(group)
page.within '.users-group-form' do page.within '.invite-users-form' do
select2(current_user.id, from: '#user_ids', multiple: true) select2(current_user.id, from: '#user_ids', multiple: true)
select 'Developer', from: 'access_level' select 'Developer', from: 'access_level'
end end
click_button 'Add to group' click_button 'Invite'
page.within '.content-list' do page.within '[data-qa-selector="members_list"]' do
expect(page).to have_content(current_user.name) expect(page).to have_content(current_user.name)
expect(page).to have_content('Developer') expect(page).to have_content('Developer')
end end
...@@ -187,7 +187,7 @@ describe 'Admin Groups' do ...@@ -187,7 +187,7 @@ describe 'Admin Groups' do
visit group_group_members_path(group) visit group_group_members_path(group)
page.within '.content-list' do page.within '[data-qa-selector="members_list"]' do
expect(page).to have_content(current_user.name) expect(page).to have_content(current_user.name)
expect(page).to have_content('Developer') expect(page).to have_content('Developer')
end end
...@@ -196,7 +196,7 @@ describe 'Admin Groups' do ...@@ -196,7 +196,7 @@ describe 'Admin Groups' do
visit group_group_members_path(group) visit group_group_members_path(group)
page.within '.content-list' do page.within '[data-qa-selector="members_list"]' do
expect(page).not_to have_content(current_user.name) expect(page).not_to have_content(current_user.name)
expect(page).not_to have_content('Developer') expect(page).not_to have_content('Developer')
end end
......
...@@ -98,12 +98,12 @@ describe "Admin::Projects" do ...@@ -98,12 +98,12 @@ describe "Admin::Projects" do
it 'adds admin a to a project as developer', :js do it 'adds admin a to a project as developer', :js do
visit project_project_members_path(project) visit project_project_members_path(project)
page.within '.users-project-form' do page.within '.invite-users-form' do
select2(current_user.id, from: '#user_ids', multiple: true) select2(current_user.id, from: '#user_ids', multiple: true)
select 'Developer', from: 'access_level' select 'Developer', from: 'access_level'
end end
click_button 'Add to project' click_button 'Invite'
page.within '.content-list' do page.within '.content-list' do
expect(page).to have_content(current_user.name) expect(page).to have_content(current_user.name)
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Groups > Members > Manage groups', :js do
include Select2Helper
include Spec::Support::Helpers::Features::ListRowsHelpers
let(:user) { create(:user) }
let(:shared_with_group) { create(:group) }
let(:shared_group) { create(:group) }
before do
shared_group.add_owner(user)
sign_in(user)
end
context 'with share groups with groups feature flag' do
before do
stub_feature_flags(shared_with_group: true)
end
it 'add group to group' do
visit group_group_members_path(shared_group)
add_group(shared_with_group.id, 'Reporter')
page.within(first_row) do
expect(page).to have_content(shared_with_group.name)
expect(page).to have_content('Reporter')
end
end
it 'remove user from group' do
create(:group_group_link, shared_group: shared_group,
shared_with_group: shared_with_group, group_access: ::Gitlab::Access::DEVELOPER)
visit group_group_members_path(shared_group)
expect(page).to have_content(shared_with_group.name)
accept_confirm do
find(:css, '#existing_shares li', text: shared_with_group.name).find(:css, 'a.btn-remove').click
end
wait_for_requests
expect(page).not_to have_content(shared_with_group.name)
end
it 'update group to owner level' do
create(:group_group_link, shared_group: shared_group,
shared_with_group: shared_with_group, group_access: ::Gitlab::Access::DEVELOPER)
visit group_group_members_path(shared_group)
page.within(first_row) do
click_button('Developer')
click_link('Maintainer')
wait_for_requests
expect(page).to have_button('Maintainer')
end
end
def add_group(id, role)
page.click_link 'Invite group'
page.within ".invite-group-form" do
select2(id, from: "#shared_with_group_id")
select(role, from: "shared_group_access")
click_button "Invite"
end
end
end
context 'without share groups with groups feature flag' do
before do
stub_feature_flags(share_group_with_group: false)
end
it 'does not render invitation form and tabs' do
visit group_group_members_path(shared_group)
expect(page).not_to have_link('Invite member')
expect(page).not_to have_link('Invite group')
end
end
end
...@@ -113,7 +113,8 @@ describe 'Groups > Members > Manage members' do ...@@ -113,7 +113,8 @@ describe 'Groups > Members > Manage members' do
visit group_group_members_path(group) visit group_group_members_path(group)
expect(page).not_to have_button 'Add to group' expect(page).not_to have_selector '.invite-users-form'
expect(page).not_to have_selector '.invite-group-form'
page.within(second_row) do page.within(second_row) do
# Can not modify user2 role # Can not modify user2 role
...@@ -125,11 +126,10 @@ describe 'Groups > Members > Manage members' do ...@@ -125,11 +126,10 @@ describe 'Groups > Members > Manage members' do
end end
def add_user(id, role) def add_user(id, role)
page.within ".users-group-form" do page.within ".invite-users-form" do
select2(id, from: "#user_ids", multiple: true) select2(id, from: "#user_ids", multiple: true)
select(role, from: "access_level") select(role, from: "access_level")
click_button "Invite"
end end
click_button "Add to group"
end end
end end
...@@ -24,7 +24,7 @@ describe 'Search group member' do ...@@ -24,7 +24,7 @@ describe 'Search group member' do
find('.user-search-btn').click find('.user-search-btn').click
end end
group_members_list = find(".card .content-list") group_members_list = find('[data-qa-selector="members_list"]')
expect(group_members_list).to have_content(member.name) expect(group_members_list).to have_content(member.name)
expect(group_members_list).not_to have_content(user.name) expect(group_members_list).not_to have_content(user.name)
end end
......
...@@ -87,12 +87,12 @@ describe 'Project members list' do ...@@ -87,12 +87,12 @@ describe 'Project members list' do
end end
def add_user(id, role) def add_user(id, role)
page.within ".users-project-form" do page.within ".invite-users-form" do
select2(id, from: "#user_ids", multiple: true) select2(id, from: "#user_ids", multiple: true)
select(role, from: "access_level") select(role, from: "access_level")
end end
click_button "Add to project" click_button "Invite"
end end
def visit_members_page def visit_members_page
......
...@@ -20,10 +20,10 @@ describe 'Projects > Members > Maintainer adds member with expiration date', :js ...@@ -20,10 +20,10 @@ describe 'Projects > Members > Maintainer adds member with expiration date', :js
date = 4.days.from_now date = 4.days.from_now
visit project_project_members_path(project) visit project_project_members_path(project)
page.within '.users-project-form' do page.within '.invite-users-form' do
select2(new_member.id, from: '#user_ids', multiple: true) select2(new_member.id, from: '#user_ids', multiple: true)
fill_in 'expires_at', with: date.to_s(:medium) + "\n" fill_in 'expires_at', with: date.to_s(:medium) + "\n"
click_on 'Add to project' click_on 'Invite'
end end
page.within "#project_member_#{new_member.project_members.first.id}" do page.within "#project_member_#{new_member.project_members.first.id}" do
......
...@@ -37,7 +37,7 @@ describe 'Projects > Settings > User manages project members' do ...@@ -37,7 +37,7 @@ describe 'Projects > Settings > User manages project members' do
visit(project_project_members_path(project)) visit(project_project_members_path(project))
page.within('.users-project-form') do page.within('.invite-users-form') do
click_link('Import') click_link('Import')
end end
......
...@@ -10,6 +10,7 @@ describe GroupMembersFinder, '#execute' do ...@@ -10,6 +10,7 @@ describe GroupMembersFinder, '#execute' do
let(:user2) { create(:user) } let(:user2) { create(:user) }
let(:user3) { create(:user) } let(:user3) { create(:user) }
let(:user4) { create(:user) } let(:user4) { create(:user) }
let(:user5) { create(:user, :two_factor_via_otp) }
it 'returns members for top-level group' do it 'returns members for top-level group' do
member1 = group.add_maintainer(user1) member1 = group.add_maintainer(user1)
...@@ -67,4 +68,43 @@ describe GroupMembersFinder, '#execute' do ...@@ -67,4 +68,43 @@ describe GroupMembersFinder, '#execute' do
expect(result.to_a).to match_array([member1, member2, member3, member4]) expect(result.to_a).to match_array([member1, member2, member3, member4])
end end
it 'returns searched members if requested' do
group.add_maintainer(user2)
nested_group.add_maintainer(user2)
nested_group.add_maintainer(user3)
nested_group.add_maintainer(user4)
member = group.add_maintainer(user1)
result = described_class.new(group).execute(include_relations: [:direct, :descendants], params: { search: user1.name })
expect(result.to_a).to match_array([member])
end
it 'returns members with two-factor auth if requested by owner' do
group.add_owner(user2)
group.add_maintainer(user1)
nested_group.add_maintainer(user2)
nested_group.add_maintainer(user3)
nested_group.add_maintainer(user4)
member = group.add_maintainer(user5)
result = described_class.new(group, user2).execute(include_relations: [:direct, :descendants], params: { two_factor: 'enabled' })
expect(result.to_a).to contain_exactly(member)
end
it 'returns members without two-factor auth if requested by owner' do
member1 = group.add_owner(user2)
member2 = group.add_maintainer(user1)
nested_group.add_maintainer(user2)
member3 = nested_group.add_maintainer(user3)
member4 = nested_group.add_maintainer(user4)
member_with_2fa = group.add_maintainer(user5)
result = described_class.new(group, user2).execute(include_relations: [:direct, :descendants], params: { two_factor: 'disabled' })
expect(result.to_a).not_to include(member_with_2fa)
expect(result.to_a).to match_array([member1, member2, member3, member4])
end
end end
Return-Path: <jake@example.com>
Received: from myserver.example.com ([unix socket]) by myserver (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
Received: from mail.example.com (mail.example.com [IPv6:2607:f8b0:4001:c03::234]) by myserver.example.com (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@example.com>; Thu, 13 Jun 2013 17:03:50 -0400
Received: by myserver.example.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.example.com>; Thu, 13 Jun 2013 14:03:48 -0700
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
From: "jake@example.com" <jake@example.com>
To: "support@example.com" <support@example.com>
Subject: Insert hilarious subject line here
Date: Tue, 26 Nov 2019 14:22:41 +0000
Message-ID: <7e2296f83dbf4de388cbf5f56f52c11f@EXDAG29-1.EXCHANGE.INT>
Accept-Language: de-DE, en-US
Content-Language: de-DE
X-MS-Has-Attach:
X-MS-TNEF-Correlator:
x-ms-exchange-transport-fromentityheader: Hosted
x-originating-ip: [62.96.54.178]
Content-Type: multipart/alternative;
boundary="_000_7e2296f83dbf4de388cbf5f56f52c11fEXDAG291EXCHANGEINT_"
MIME-Version: 1.0
Envelope-To: incoming+gitlabhq/gitlabhq+auth_token@appmail.example.com
--_000_7e2296f83dbf4de388cbf5f56f52c11fEXDAG291EXCHANGEINT_
Content-Type: text/plain; charset="iso-8859-1"
Content-Transfer-Encoding: quoted-printable
--_000_7e2296f83dbf4de388cbf5f56f52c11fEXDAG291EXCHANGEINT_
Content-Type: text/html; charset="iso-8859-1"
Content-Transfer-Encoding: quoted-printable
Look, a message with some alternate headers! We should really support them.
Delivered-To: incoming+gitlabhq/gitlabhq+auth_token@appmail.adventuretime.ooo Delivered-To: incoming+gitlabhq/gitlabhq+auth_token@appmail.example.com
Return-Path: <jake@adventuretime.ooo> Return-Path: <jake@example.com>
Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400 Received: from iceking.example.com ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400 Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.example.com (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.example.com>; Thu, 13 Jun 2013 17:03:50 -0400
Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700 Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.example.com>; Thu, 13 Jun 2013 14:03:48 -0700
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
Date: Thu, 13 Jun 2013 17:03:48 -0400 Date: Thu, 13 Jun 2013 17:03:48 -0400
From: Jake the Dog <jake@adventuretime.ooo> From: Jake the Dog <jake@example.com>
Delivered-To: support@adventuretime.ooo Delivered-To: support@example.com
To: support@adventuretime.ooo To: support@example.com
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
Subject: New Issue by email Subject: New Issue by email
Mime-Version: 1.0 Mime-Version: 1.0
......
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Settings Form renders 1`] = ` exports[`Settings Form renders 1`] = `
<div <form>
class="card" <div
> class="card"
<form> >
<!---->
<div <div
class="card-header" class="card-header"
> >
...@@ -12,11 +13,13 @@ exports[`Settings Form renders 1`] = ` ...@@ -12,11 +13,13 @@ exports[`Settings Form renders 1`] = `
Tag expiration policy Tag expiration policy
</div> </div>
<div <div
class="card-body" class="card-body"
> >
<gl-form-group-stub <!---->
<!---->
<glformgroup-stub
id="expiration-policy-toggle-group" id="expiration-policy-toggle-group"
label="Expiration policy:" label="Expiration policy:"
label-align="right" label-align="right"
...@@ -26,7 +29,7 @@ exports[`Settings Form renders 1`] = ` ...@@ -26,7 +29,7 @@ exports[`Settings Form renders 1`] = `
<div <div
class="d-flex align-items-start" class="d-flex align-items-start"
> >
<gl-toggle-stub <gltoggle-stub
id="expiration-policy-toggle" id="expiration-policy-toggle"
labeloff="Toggle Status: OFF" labeloff="Toggle Status: OFF"
labelon="Toggle Status: ON" labelon="Toggle Status: ON"
...@@ -41,81 +44,96 @@ exports[`Settings Form renders 1`] = ` ...@@ -41,81 +44,96 @@ exports[`Settings Form renders 1`] = `
</strong> </strong>
</span> </span>
</div> </div>
</gl-form-group-stub> </glformgroup-stub>
<gl-form-group-stub <glformgroup-stub
id="expiration-policy-interval-group" id="expiration-policy-interval-group"
label="Expiration interval:" label="Expiration interval:"
label-align="right" label-align="right"
label-cols="3" label-cols="3"
label-for="expiration-policy-interval" label-for="expiration-policy-interval"
> >
<gl-form-select-stub <glformselect-stub
disabled="true"
id="expiration-policy-interval" id="expiration-policy-interval"
value="bar"
> >
<option <option
value="1" value="foo"
> >
Option 1
Foo
</option> </option>
<option <option
value="2" value="bar"
> >
Option 2
Bar
</option> </option>
</gl-form-select-stub> </glformselect-stub>
</gl-form-group-stub> </glformgroup-stub>
<gl-form-group-stub <glformgroup-stub
id="expiration-policy-schedule-group" id="expiration-policy-schedule-group"
label="Expiration schedule:" label="Expiration schedule:"
label-align="right" label-align="right"
label-cols="3" label-cols="3"
label-for="expiration-policy-schedule" label-for="expiration-policy-schedule"
> >
<gl-form-select-stub <glformselect-stub
disabled="true"
id="expiration-policy-schedule" id="expiration-policy-schedule"
value="bar"
> >
<option <option
value="1" value="foo"
> >
Option 1
Foo
</option> </option>
<option <option
value="2" value="bar"
> >
Option 2
Bar
</option> </option>
</gl-form-select-stub> </glformselect-stub>
</gl-form-group-stub> </glformgroup-stub>
<gl-form-group-stub <glformgroup-stub
id="expiration-policy-latest-group" id="expiration-policy-latest-group"
label="Expiration latest:" label="Expiration latest:"
label-align="right" label-align="right"
label-cols="3" label-cols="3"
label-for="expiration-policy-latest" label-for="expiration-policy-latest"
> >
<gl-form-select-stub <glformselect-stub
disabled="true"
id="expiration-policy-latest" id="expiration-policy-latest"
value="bar"
> >
<option <option
value="1" value="foo"
> >
Option 1
Foo
</option> </option>
<option <option
value="2" value="bar"
> >
Option 2
Bar
</option> </option>
</gl-form-select-stub> </glformselect-stub>
</gl-form-group-stub> </glformgroup-stub>
<gl-form-group-stub <glformgroup-stub
id="expiration-policy-name-matching-group" id="expiration-policy-name-matching-group"
invalid-feedback="The value of this input should be less than 255 characters" invalid-feedback="The value of this input should be less than 255 characters"
label="Expire Docker tags with name matching:" label="Expire Docker tags with name matching:"
...@@ -123,33 +141,41 @@ exports[`Settings Form renders 1`] = ` ...@@ -123,33 +141,41 @@ exports[`Settings Form renders 1`] = `
label-cols="3" label-cols="3"
label-for="expiration-policy-name-matching" label-for="expiration-policy-name-matching"
> >
<gl-form-textarea-stub <glformtextarea-stub
disabled="true"
id="expiration-policy-name-matching" id="expiration-policy-name-matching"
placeholder=".*" placeholder=".*"
trim="" trim=""
value="" value=""
/> />
</gl-form-group-stub> </glformgroup-stub>
</div> </div>
<div <div
class="card-footer text-right" class="card-footer"
> >
<gl-button-stub <div
type="reset" class="d-flex justify-content-end"
>
Cancel
</gl-button-stub>
<gl-button-stub
type="submit"
variant="success"
> >
<glbutton-stub
class="mr-2 d-block"
type="reset"
>
Cancel
</glbutton-stub>
<glbutton-stub
class="d-block"
type="submit"
variant="success"
>
Save expiration policy
Save Expiration Policy </glbutton-stub>
</div>
</gl-button-stub>
</div> </div>
</form> <!---->
</div> </div>
</form>
`; `;
import Vuex from 'vuex'; import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { mount, createLocalVue } from '@vue/test-utils';
import stubChildren from 'helpers/stub_children';
import component from '~/registry/settings/components/settings_form.vue'; import component from '~/registry/settings/components/settings_form.vue';
import { createStore } from '~/registry/settings/store/'; import { createStore } from '~/registry/settings/store/';
import { NAME_REGEX_LENGTH } from '~/registry/settings/constants'; import { NAME_REGEX_LENGTH } from '~/registry/settings/constants';
import { stringifiedFormOptions } from '../mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
...@@ -13,7 +15,6 @@ describe('Settings Form', () => { ...@@ -13,7 +15,6 @@ describe('Settings Form', () => {
let saveSpy; let saveSpy;
let resetSpy; let resetSpy;
const helpPagePath = 'foo';
const findFormGroup = name => wrapper.find(`#expiration-policy-${name}-group`); const findFormGroup = name => wrapper.find(`#expiration-policy-${name}-group`);
const findFormElements = (name, father = wrapper) => father.find(`#expiration-policy-${name}`); const findFormElements = (name, father = wrapper) => father.find(`#expiration-policy-${name}`);
const findCancelButton = () => wrapper.find({ ref: 'cancel-button' }); const findCancelButton = () => wrapper.find({ ref: 'cancel-button' });
...@@ -23,7 +24,11 @@ describe('Settings Form', () => { ...@@ -23,7 +24,11 @@ describe('Settings Form', () => {
const mountComponent = (options = {}) => { const mountComponent = (options = {}) => {
saveSpy = jest.fn(); saveSpy = jest.fn();
resetSpy = jest.fn(); resetSpy = jest.fn();
wrapper = shallowMount(component, { wrapper = mount(component, {
stubs: {
...stubChildren(component),
GlCard: false,
},
store, store,
methods: { methods: {
saveSettings: saveSpy, saveSettings: saveSpy,
...@@ -35,7 +40,7 @@ describe('Settings Form', () => { ...@@ -35,7 +40,7 @@ describe('Settings Form', () => {
beforeEach(() => { beforeEach(() => {
store = createStore(); store = createStore();
store.dispatch('setInitialState', { helpPagePath }); store.dispatch('setInitialState', stringifiedFormOptions);
mountComponent(); mountComponent();
}); });
...@@ -48,13 +53,13 @@ describe('Settings Form', () => { ...@@ -48,13 +53,13 @@ describe('Settings Form', () => {
}); });
describe.each` describe.each`
elementName | modelName | value elementName | modelName | value | disabledByToggle
${'toggle'} | ${'enabled'} | ${true} ${'toggle'} | ${'enabled'} | ${true} | ${'not disabled'}
${'interval'} | ${'older_than'} | ${'foo'} ${'interval'} | ${'older_than'} | ${'foo'} | ${'disabled'}
${'schedule'} | ${'cadence'} | ${'foo'} ${'schedule'} | ${'cadence'} | ${'foo'} | ${'disabled'}
${'latest'} | ${'keep_n'} | ${'foo'} ${'latest'} | ${'keep_n'} | ${'foo'} | ${'disabled'}
${'name-matching'} | ${'name_regex'} | ${'foo'} ${'name-matching'} | ${'name_regex'} | ${'foo'} | ${'disabled'}
`('%s form element', ({ elementName, modelName, value }) => { `('$elementName form element', ({ elementName, modelName, value, disabledByToggle }) => {
let formGroup; let formGroup;
beforeEach(() => { beforeEach(() => {
formGroup = findFormGroup(elementName); formGroup = findFormGroup(elementName);
...@@ -89,6 +94,12 @@ describe('Settings Form', () => { ...@@ -89,6 +94,12 @@ describe('Settings Form', () => {
expect(wrapper.vm[modelName]).toBe(value); expect(wrapper.vm[modelName]).toBe(value);
}); });
}); });
it(`${elementName} is ${disabledByToggle} by enabled set to false`, () => {
store.dispatch('updateSettings', { enabled: false });
const expectation = disabledByToggle === 'disabled' ? 'true' : undefined;
expect(findFormElements(elementName, formGroup).attributes('disabled')).toBe(expectation);
});
}); });
describe('form actions', () => { describe('form actions', () => {
......
export const options = [{ key: 'foo', label: 'Foo' }, { key: 'bar', label: 'Bar', default: true }];
export const stringifiedOptions = JSON.stringify(options);
export const stringifiedFormOptions = {
cadenceOptions: stringifiedOptions,
keepNOptions: stringifiedOptions,
olderThanOptions: stringifiedOptions,
};
export const formOptions = {
cadence: options,
keepN: options,
olderThan: options,
};
import mutations from '~/registry/settings/store/mutations'; import mutations from '~/registry/settings/store/mutations';
import * as types from '~/registry/settings/store/mutation_types'; import * as types from '~/registry/settings/store/mutation_types';
import createState from '~/registry/settings/store/state'; import createState from '~/registry/settings/store/state';
import { formOptions, stringifiedFormOptions } from '../mock_data';
describe('Mutations Registry Store', () => { describe('Mutations Registry Store', () => {
let mockState; let mockState;
...@@ -11,11 +12,14 @@ describe('Mutations Registry Store', () => { ...@@ -11,11 +12,14 @@ describe('Mutations Registry Store', () => {
describe('SET_INITIAL_STATE', () => { describe('SET_INITIAL_STATE', () => {
it('should set the initial state', () => { it('should set the initial state', () => {
const payload = { helpPagePath: 'foo', projectId: 'bar' }; const expectedState = { ...mockState, projectId: 'foo', formOptions };
const expectedState = { ...mockState, ...payload }; mutations[types.SET_INITIAL_STATE](mockState, {
mutations[types.SET_INITIAL_STATE](mockState, payload); projectId: 'foo',
...stringifiedFormOptions,
});
expect(mockState.projectId).toEqual(expectedState.projectId); expect(mockState.projectId).toEqual(expectedState.projectId);
expect(mockState.formOptions).toEqual(expectedState.formOptions);
}); });
}); });
......
...@@ -3,49 +3,77 @@ import { mapComputed } from '~/vuex_shared/bindings'; ...@@ -3,49 +3,77 @@ import { mapComputed } from '~/vuex_shared/bindings';
describe('Binding utils', () => { describe('Binding utils', () => {
describe('mapComputed', () => { describe('mapComputed', () => {
const dummyComponent = { const defaultArgs = [['baz'], 'bar', 'foo'];
const createDummy = (mapComputedArgs = defaultArgs) => ({
computed: { computed: {
...mapComputed('foo', 'bar', ['baz']), ...mapComputed(...mapComputedArgs),
}, },
render() { render() {
return null; return null;
}, },
});
const mocks = {
$store: {
state: {
baz: 2,
foo: {
baz: 1,
},
},
getters: {
getBaz: 'foo',
},
dispatch: jest.fn(),
},
}; };
it('returns an object with keys equal to the last fn parameter ', () => {
it('returns an object with keys equal to the first fn parameter ', () => {
const keyList = ['foo1', 'foo2']; const keyList = ['foo1', 'foo2'];
const result = mapComputed('foo', 'bar', keyList); const result = mapComputed(keyList, 'foo', 'bar');
expect(Object.keys(result)).toEqual(keyList); expect(Object.keys(result)).toEqual(keyList);
}); });
it('returned object has set and get function', () => { it('returned object has set and get function', () => {
const result = mapComputed('foo', 'bar', ['baz']); const result = mapComputed(['baz'], 'foo', 'bar');
expect(result.baz.set).toBeDefined(); expect(result.baz.set).toBeDefined();
expect(result.baz.get).toBeDefined(); expect(result.baz.get).toBeDefined();
}); });
it('set function invokes $store.dispatch', () => { describe('set function', () => {
const context = shallowMount(dummyComponent, { it('invokes $store.dispatch', () => {
mocks: { const context = shallowMount(createDummy(), { mocks });
$store: { context.vm.baz = 'a';
dispatch: jest.fn(), expect(context.vm.$store.dispatch).toHaveBeenCalledWith('bar', { baz: 'a' });
}, });
}, it('uses updateFn in list object mode if updateFn exists', () => {
const context = shallowMount(createDummy([[{ key: 'foo', updateFn: 'baz' }]]), { mocks });
context.vm.foo = 'b';
expect(context.vm.$store.dispatch).toHaveBeenCalledWith('baz', { foo: 'b' });
});
it('in list object mode defaults to defaultUpdateFn if updateFn do not exists', () => {
const context = shallowMount(createDummy([[{ key: 'foo' }], 'defaultFn']), { mocks });
context.vm.foo = 'c';
expect(context.vm.$store.dispatch).toHaveBeenCalledWith('defaultFn', { foo: 'c' });
}); });
context.vm.baz = 'a';
expect(context.vm.$store.dispatch).toHaveBeenCalledWith('bar', { baz: 'a' });
}); });
it('get function returns $store.state[root][key]', () => {
const context = shallowMount(dummyComponent, { describe('get function', () => {
mocks: { it('if root is set returns $store.state[root][key]', () => {
$store: { const context = shallowMount(createDummy(), { mocks });
state: { expect(context.vm.baz).toBe(mocks.$store.state.foo.baz);
foo: { });
baz: 1,
}, it('if root is not set returns $store.state[key]', () => {
}, const context = shallowMount(createDummy([['baz'], 'bar']), { mocks });
}, expect(context.vm.baz).toBe(mocks.$store.state.baz);
}, });
it('when using getters it invoke the appropriate getter', () => {
const context = shallowMount(createDummy([[{ getter: 'getBaz', key: 'baz' }]]), { mocks });
expect(context.vm.baz).toBe(mocks.$store.getters.getBaz);
}); });
expect(context.vm.baz).toBe(1);
}); });
}); });
}); });
...@@ -76,6 +76,20 @@ describe ApplicationSettingsHelper do ...@@ -76,6 +76,20 @@ describe ApplicationSettingsHelper do
) )
end end
it 'returns delete_self_monitoring_project_path' do
expect(helper.self_monitoring_project_data).to include(
'delete_self_monitoring_project_path' =>
delete_self_monitoring_project_admin_application_settings_path
)
end
it 'returns status_delete_self_monitoring_project_path' do
expect(helper.self_monitoring_project_data).to include(
'status_delete_self_monitoring_project_path' =>
status_delete_self_monitoring_project_admin_application_settings_path
)
end
it 'returns self_monitoring_project_exists false' do it 'returns self_monitoring_project_exists false' do
expect(helper.self_monitoring_project_data).to include( expect(helper.self_monitoring_project_data).to include(
'self_monitoring_project_exists' => false 'self_monitoring_project_exists' => false
......
...@@ -5,22 +5,27 @@ require 'spec_helper' ...@@ -5,22 +5,27 @@ require 'spec_helper'
describe Gitlab::Email::Receiver do describe Gitlab::Email::Receiver do
include_context :email_shared_context include_context :email_shared_context
context "when the email contains a valid email address in a Delivered-To header" do context 'when the email contains a valid email address in a header' do
let(:email_raw) { fixture_file('emails/forwarded_new_issue.eml') }
let(:handler) { double(:handler) } let(:handler) { double(:handler) }
before do before do
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
allow(handler).to receive(:execute) allow(handler).to receive(:execute)
allow(handler).to receive(:metrics_params) allow(handler).to receive(:metrics_params)
allow(handler).to receive(:metrics_event) allow(handler).to receive(:metrics_event)
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.example.com")
end
context 'when in a Delivered-To header' do
let(:email_raw) { fixture_file('emails/forwarded_new_issue.eml') }
it_behaves_like 'correctly finds the mail key'
end end
it "finds the mail key" do context 'when in an Envelope-To header' do
expect(Gitlab::Email::Handler).to receive(:for).with(an_instance_of(Mail::Message), 'gitlabhq/gitlabhq+auth_token').and_return(handler) let(:email_raw) { fixture_file('emails/envelope_to_header.eml') }
receiver.execute it_behaves_like 'correctly finds the mail key'
end end
end end
......
...@@ -229,6 +229,59 @@ describe Gitlab::GitalyClient do ...@@ -229,6 +229,59 @@ describe Gitlab::GitalyClient do
end end
end end
end end
context 'deadlines', :request_store do
let(:request_deadline) { real_time + 10.0 }
before do
allow(Gitlab::RequestContext.instance).to receive(:request_deadline).and_return(request_deadline)
end
it 'includes the deadline information' do
kword_args = described_class.request_kwargs('default', timeout: 2)
expect(kword_args[:deadline])
.to be_within(1).of(real_time + 2)
expect(kword_args[:metadata][:deadline_type]).to eq("regular")
end
it 'limits the deadline do the request deadline if that is closer', :aggregate_failures do
kword_args = described_class.request_kwargs('default', timeout: 15)
expect(kword_args[:deadline]).to eq(request_deadline)
expect(kword_args[:metadata][:deadline_type]).to eq("limited")
end
it 'does not limit calls in sidekiq' do
expect(Sidekiq).to receive(:server?).and_return(true)
kword_args = described_class.request_kwargs('default', timeout: 6.hours.to_i)
expect(kword_args[:deadline]).to be_within(1).of(real_time + 6.hours.to_i)
expect(kword_args[:metadata][:deadline_type]).to be_nil
end
it 'does not limit calls in sidekiq when allowed unlimited' do
expect(Sidekiq).to receive(:server?).and_return(true)
kword_args = described_class.request_kwargs('default', timeout: 0)
expect(kword_args[:deadline]).to be_nil
expect(kword_args[:metadata][:deadline_type]).to be_nil
end
it 'includes only the deadline specified by the timeout when there was no deadline' do
allow(Gitlab::RequestContext.instance).to receive(:request_deadline).and_return(nil)
kword_args = described_class.request_kwargs('default', timeout: 6.hours.to_i)
expect(kword_args[:deadline]).to be_within(1).of(Gitlab::Metrics::System.real_time + 6.hours.to_i)
expect(kword_args[:metadata][:deadline_type]).to be_nil
end
def real_time
Gitlab::Metrics::System.real_time
end
end
end end
describe 'enforce_gitaly_request_limits?' do describe 'enforce_gitaly_request_limits?' do
......
# frozen_string_literal: true
require 'fast_spec_helper'
require 'rack'
require 'request_store'
require_relative '../../../support/helpers/next_instance_of'
describe Gitlab::Middleware::RequestContext do
include NextInstanceOf
let(:app) { -> (env) {} }
let(:env) { {} }
around do |example|
RequestStore.begin!
example.run
RequestStore.end!
RequestStore.clear!
end
describe '#call' do
context 'setting the client ip' do
subject { Gitlab::RequestContext.instance.client_ip }
context 'with X-Forwarded-For headers' do
let(:load_balancer_ip) { '1.2.3.4' }
let(:headers) do
{
'HTTP_X_FORWARDED_FOR' => "#{load_balancer_ip}, 127.0.0.1",
'REMOTE_ADDR' => '127.0.0.1'
}
end
let(:env) { Rack::MockRequest.env_for("/").merge(headers) }
it 'returns the load balancer IP' do
endpoint = proc do
[200, {}, ["Hello"]]
end
described_class.new(endpoint).call(env)
expect(subject).to eq(load_balancer_ip)
end
end
context 'request' do
let(:ip) { '192.168.1.11' }
before do
allow_next_instance_of(Rack::Request) do |instance|
allow(instance).to receive(:ip).and_return(ip)
end
described_class.new(app).call(env)
end
it { is_expected.to eq(ip) }
end
context 'before RequestContext middleware run' do
it { is_expected.to be_nil }
end
end
end
context 'setting the thread cpu time' do
it 'sets the `start_thread_cpu_time`' do
expect { described_class.new(app).call(env) }
.to change { Gitlab::RequestContext.instance.start_thread_cpu_time }.from(nil).to(Float)
end
end
context 'setting the request start time' do
it 'sets the `request_start_time`' do
expect { described_class.new(app).call(env) }
.to change { Gitlab::RequestContext.instance.request_start_time }.from(nil).to(Float)
end
end
end
...@@ -2,59 +2,44 @@ ...@@ -2,59 +2,44 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::RequestContext do describe Gitlab::RequestContext, :request_store do
describe '#client_ip' do subject { described_class.instance }
subject { described_class.client_ip }
let(:app) { -> (env) {} } it { is_expected.to have_attributes(client_ip: nil, start_thread_cpu_time: nil, request_start_time: nil) }
let(:env) { Hash.new }
context 'with X-Forwarded-For headers', :request_store do describe '#request_deadline' do
let(:load_balancer_ip) { '1.2.3.4' } let(:request_start_time) { 1575982156.206008 }
let(:headers) do
{
'HTTP_X_FORWARDED_FOR' => "#{load_balancer_ip}, 127.0.0.1",
'REMOTE_ADDR' => '127.0.0.1'
}
end
let(:env) { Rack::MockRequest.env_for("/").merge(headers) } it "sets the time to #{Settings.gitlab.max_request_duration_seconds} seconds in the future" do
allow(subject).to receive(:request_start_time).and_return(request_start_time)
it 'returns the load balancer IP' do expect(subject.request_deadline).to eq(1575982156.206008 + Settings.gitlab.max_request_duration_seconds)
client_ip = nil expect(subject.request_deadline).to be_a(Float)
end
endpoint = proc do
client_ip = Gitlab::SafeRequestStore[:client_ip]
[200, {}, ["Hello"]]
end
described_class.new(endpoint).call(env) it 'returns nil if there is no start time' do
allow(subject).to receive(:request_start_time).and_return(nil)
expect(client_ip).to eq(load_balancer_ip) expect(subject.request_deadline).to be_nil
end
end end
end
context 'when RequestStore::Middleware is used' do describe '#ensure_request_deadline_not_exceeded!' do
around do |example| it 'does not raise an error when there was no deadline' do
RequestStore::Middleware.new(-> (env) { example.run }).call({}) expect(subject).to receive(:request_deadline).and_return(nil)
end expect { subject.ensure_deadline_not_exceeded! }.not_to raise_error
end
context 'request' do it 'does not raise an error if the deadline is in the future' do
let(:ip) { '192.168.1.11' } allow(subject).to receive(:request_deadline).and_return(Gitlab::Metrics::System.real_time + 10)
before do expect { subject.ensure_deadline_not_exceeded! }.not_to raise_error
allow_next_instance_of(Rack::Request) do |instance| end
allow(instance).to receive(:ip).and_return(ip)
end
described_class.new(app).call(env)
end
it { is_expected.to eq(ip) } it 'raises an error when the deadline is in the past' do
end allow(subject).to receive(:request_deadline).and_return(Gitlab::Metrics::System.real_time - 10)
context 'before RequestContext middleware run' do expect { subject.ensure_deadline_not_exceeded! }.to raise_error(described_class::RequestDeadlineExceeded)
it { is_expected.to be_nil }
end
end end
end end
end end
...@@ -319,6 +319,11 @@ describe ApplicationSetting do ...@@ -319,6 +319,11 @@ describe ApplicationSetting do
end end
context 'gitaly timeouts' do context 'gitaly timeouts' do
it "validates that the default_timeout is lower than the max_request_duration" do
is_expected.to validate_numericality_of(:gitaly_timeout_default)
.is_less_than_or_equal_to(Settings.gitlab.max_request_duration_seconds)
end
[:gitaly_timeout_default, :gitaly_timeout_medium, :gitaly_timeout_fast].each do |timeout_name| [:gitaly_timeout_default, :gitaly_timeout_medium, :gitaly_timeout_fast].each do |timeout_name|
it do it do
is_expected.to validate_presence_of(timeout_name) is_expected.to validate_presence_of(timeout_name)
......
...@@ -33,4 +33,12 @@ describe GroupGroupLink do ...@@ -33,4 +33,12 @@ describe GroupGroupLink do
validate_inclusion_of(:group_access).in_array(Gitlab::Access.values)) validate_inclusion_of(:group_access).in_array(Gitlab::Access.values))
end end
end end
describe '#human_access' do
it 'delegates to Gitlab::Access' do
expect(Gitlab::Access).to receive(:human_access).with(group_group_link.group_access)
group_group_link.human_access
end
end
end end
...@@ -1003,6 +1003,57 @@ describe Group do ...@@ -1003,6 +1003,57 @@ describe Group do
end end
end end
describe '#related_group_ids' do
let(:nested_group) { create(:group, parent: group) }
let(:shared_with_group) { create(:group, parent: group) }
before do
create(:group_group_link, shared_group: nested_group,
shared_with_group: shared_with_group)
end
subject(:related_group_ids) { nested_group.related_group_ids }
it 'returns id' do
expect(related_group_ids).to include(nested_group.id)
end
it 'returns ancestor id' do
expect(related_group_ids).to include(group.id)
end
it 'returns shared with group id' do
expect(related_group_ids).to include(shared_with_group.id)
end
context 'with more than one ancestor group' do
let(:ancestor_group) { create(:group) }
before do
group.update(parent: ancestor_group)
end
it 'returns all ancestor group ids' do
expect(related_group_ids).to(
include(group.id, ancestor_group.id))
end
end
context 'with more than one shared with group' do
let(:another_shared_with_group) { create(:group, parent: group) }
before do
create(:group_group_link, shared_group: nested_group,
shared_with_group: another_shared_with_group)
end
it 'returns all shared with group ids' do
expect(related_group_ids).to(
include(shared_with_group.id, another_shared_with_group.id))
end
end
end
context 'with uploads' do context 'with uploads' do
it_behaves_like 'model with uploads', true do it_behaves_like 'model with uploads', true do
let(:model_object) { create(:group, :with_avatar) } let(:model_object) { create(:group, :with_avatar) }
......
...@@ -60,7 +60,7 @@ describe 'Self-Monitoring project requests' do ...@@ -60,7 +60,7 @@ describe 'Self-Monitoring project requests' do
end end
it_behaves_like 'sets polling header and returns accepted' do it_behaves_like 'sets polling header and returns accepted' do
let(:in_progress_message) { 'Job is in progress' } let(:in_progress_message) { 'Job to create self-monitoring project is in progress' }
end end
end end
...@@ -115,4 +115,110 @@ describe 'Self-Monitoring project requests' do ...@@ -115,4 +115,110 @@ describe 'Self-Monitoring project requests' do
end end
end end
end end
describe 'DELETE #delete_self_monitoring_project' do
let(:worker_class) { SelfMonitoringProjectDeleteWorker }
subject { delete delete_self_monitoring_project_admin_application_settings_path }
it_behaves_like 'not accessible to non-admin users'
context 'with admin user' do
before do
login_as(admin)
end
context 'with feature flag disabled' do
it_behaves_like 'not accessible if feature flag is disabled'
end
context 'with feature flag enabled' do
let(:status_api) { status_delete_self_monitoring_project_admin_application_settings_path }
it_behaves_like 'triggers async worker, returns sidekiq job_id with response accepted'
end
end
end
describe 'GET #status_delete_self_monitoring_project' do
let(:worker_class) { SelfMonitoringProjectDeleteWorker }
let(:job_id) { 'job_id' }
subject do
get status_delete_self_monitoring_project_admin_application_settings_path,
params: { job_id: job_id }
end
it_behaves_like 'not accessible to non-admin users'
context 'with admin user' do
before do
login_as(admin)
end
context 'with feature flag disabled' do
it_behaves_like 'not accessible if feature flag is disabled'
end
context 'with feature flag enabled' do
it_behaves_like 'handles invalid job_id'
context 'when job is in progress' do
before do
allow(worker_class).to receive(:in_progress?)
.with(job_id)
.and_return(true)
stub_application_setting(instance_administration_project_id: 1)
end
it_behaves_like 'sets polling header and returns accepted' do
let(:in_progress_message) { 'Job to delete self-monitoring project is in progress' }
end
end
context 'when self-monitoring project exists and job does not exist' do
before do
stub_application_setting(instance_administration_project_id: 1)
end
it 'returns bad_request' do
subject
aggregate_failures do
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq(
'message' => 'Self-monitoring project was not deleted. Please check logs ' \
'for any error messages'
)
end
end
end
context 'when self-monitoring project does not exist' do
it 'does not need job_id' do
get status_delete_self_monitoring_project_admin_application_settings_path
aggregate_failures do
expect(response).to have_gitlab_http_status(:success)
expect(json_response).to eq(
'message' => 'Self-monitoring project has been successfully deleted'
)
end
end
it 'returns success with job_id' do
subject
aggregate_failures do
expect(response).to have_gitlab_http_status(:success)
expect(json_response).to eq(
'message' => 'Self-monitoring project has been successfully deleted'
)
end
end
end
end
end
end
end end
# frozen_string_literal: true
shared_examples_for 'correctly finds the mail key' do
specify do
expect(Gitlab::Email::Handler).to receive(:for).with(an_instance_of(Mail::Message), 'gitlabhq/gitlabhq+auth_token').and_return(handler)
receiver.execute
end
end
...@@ -25,7 +25,7 @@ RSpec.shared_examples 'Maintainer manages access requests' do ...@@ -25,7 +25,7 @@ RSpec.shared_examples 'Maintainer manages access requests' do
expect_no_visible_access_request(entity, user) expect_no_visible_access_request(entity, user)
page.within('.members-list') do page.within('[data-qa-selector="members_list"]') do
expect(page).to have_content user.name expect(page).to have_content user.name
end end
end end
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
shared_context 'unique ips sign in limit' do shared_context 'unique ips sign in limit' do
include StubENV include StubENV
let(:request_context) { Gitlab::RequestContext.instance }
before do before do
Gitlab::Redis::Cache.with(&:flushall) Gitlab::Redis::Cache.with(&:flushall)
Gitlab::Redis::Queues.with(&:flushall) Gitlab::Redis::Queues.with(&:flushall)
...@@ -15,10 +17,13 @@ shared_context 'unique ips sign in limit' do ...@@ -15,10 +17,13 @@ shared_context 'unique ips sign in limit' do
unique_ips_limit_enabled: true, unique_ips_limit_enabled: true,
unique_ips_limit_time_window: 10000 unique_ips_limit_time_window: 10000
) )
# Make sure we're working with the same reqeust context everywhere
allow(Gitlab::RequestContext).to receive(:instance).and_return(request_context)
end end
def change_ip(ip) def change_ip(ip)
allow(Gitlab::RequestContext).to receive(:client_ip).and_return(ip) allow(request_context).to receive(:client_ip).and_return(ip)
end end
def request_from_ip(ip) def request_from_ip(ip)
......
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