Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
06bcbc77e4
commit
b7b44de429
|
|
@ -164,8 +164,8 @@ overrides:
|
||||||
#'@graphql-eslint/unique-fragment-name': error
|
#'@graphql-eslint/unique-fragment-name': error
|
||||||
# TODO: Uncomment these rules when then `schema` is available
|
# TODO: Uncomment these rules when then `schema` is available
|
||||||
#'@graphql-eslint/fragments-on-composite-type': error
|
#'@graphql-eslint/fragments-on-composite-type': error
|
||||||
#'@graphql-eslint/known-argument-names': error
|
'@graphql-eslint/known-argument-names': error
|
||||||
#'@graphql-eslint/known-type-names': error
|
'@graphql-eslint/known-type-names': error
|
||||||
'@graphql-eslint/no-anonymous-operations': error
|
'@graphql-eslint/no-anonymous-operations': error
|
||||||
'@graphql-eslint/unique-operation-name': error
|
'@graphql-eslint/unique-operation-name': error
|
||||||
'@graphql-eslint/require-id-when-available': error
|
'@graphql-eslint/require-id-when-available': error
|
||||||
|
|
|
||||||
|
|
@ -589,7 +589,6 @@ Layout/LineLength:
|
||||||
- 'app/services/compare_service.rb'
|
- 'app/services/compare_service.rb'
|
||||||
- 'app/services/concerns/base_service_utility.rb'
|
- 'app/services/concerns/base_service_utility.rb'
|
||||||
- 'app/services/concerns/exclusive_lease_guard.rb'
|
- 'app/services/concerns/exclusive_lease_guard.rb'
|
||||||
- 'app/services/concerns/members/bulk_create_users.rb'
|
|
||||||
- 'app/services/concerns/merge_requests/assigns_merge_params.rb'
|
- 'app/services/concerns/merge_requests/assigns_merge_params.rb'
|
||||||
- 'app/services/concerns/rate_limited_service.rb'
|
- 'app/services/concerns/rate_limited_service.rb'
|
||||||
- 'app/services/concerns/schedule_bulk_repository_shard_moves_methods.rb'
|
- 'app/services/concerns/schedule_bulk_repository_shard_moves_methods.rb'
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapActions } from 'vuex';
|
export default {};
|
||||||
import CiVariableModal from './ci_variable_modal.vue';
|
|
||||||
import CiVariableTable from './ci_variable_table.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
CiVariableModal,
|
|
||||||
CiVariableTable,
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState(['isGroup']),
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
if (!this.isGroup) {
|
|
||||||
this.fetchEnvironments();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(['fetchEnvironments']),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-12">
|
<div class="col-lg-12"></div>
|
||||||
<ci-variable-table />
|
|
||||||
<ci-variable-modal />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
<script>
|
||||||
|
import { GlDropdown, GlDropdownItem, GlDropdownDivider, GlSearchBoxByType } from '@gitlab/ui';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { __, sprintf } from '~/locale';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CiEnvironmentsDropdown',
|
||||||
|
components: {
|
||||||
|
GlDropdown,
|
||||||
|
GlDropdownItem,
|
||||||
|
GlDropdownDivider,
|
||||||
|
GlSearchBoxByType,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
searchTerm: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['joinedEnvironments']),
|
||||||
|
composedCreateButtonLabel() {
|
||||||
|
return sprintf(__('Create wildcard: %{searchTerm}'), { searchTerm: this.searchTerm });
|
||||||
|
},
|
||||||
|
shouldRenderCreateButton() {
|
||||||
|
return this.searchTerm && !this.joinedEnvironments.includes(this.searchTerm);
|
||||||
|
},
|
||||||
|
filteredResults() {
|
||||||
|
const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
|
||||||
|
return this.joinedEnvironments.filter((resultString) =>
|
||||||
|
resultString.toLowerCase().includes(lowerCasedSearchTerm),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
selectEnvironment(selected) {
|
||||||
|
this.$emit('selectEnvironment', selected);
|
||||||
|
this.searchTerm = '';
|
||||||
|
},
|
||||||
|
createClicked() {
|
||||||
|
this.$emit('createClicked', this.searchTerm);
|
||||||
|
this.searchTerm = '';
|
||||||
|
},
|
||||||
|
isSelected(env) {
|
||||||
|
return this.value === env;
|
||||||
|
},
|
||||||
|
clearSearch() {
|
||||||
|
this.searchTerm = '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<gl-dropdown :text="value" @show="clearSearch">
|
||||||
|
<gl-search-box-by-type v-model.trim="searchTerm" data-testid="ci-environment-search" />
|
||||||
|
<gl-dropdown-item
|
||||||
|
v-for="environment in filteredResults"
|
||||||
|
:key="environment"
|
||||||
|
:is-checked="isSelected(environment)"
|
||||||
|
is-check-item
|
||||||
|
@click="selectEnvironment(environment)"
|
||||||
|
>
|
||||||
|
{{ environment }}
|
||||||
|
</gl-dropdown-item>
|
||||||
|
<gl-dropdown-item v-if="!filteredResults.length" ref="noMatchingResults">{{
|
||||||
|
__('No matching results')
|
||||||
|
}}</gl-dropdown-item>
|
||||||
|
<template v-if="shouldRenderCreateButton">
|
||||||
|
<gl-dropdown-divider />
|
||||||
|
<gl-dropdown-item data-testid="create-wildcard-button" @click="createClicked">
|
||||||
|
{{ composedCreateButtonLabel }}
|
||||||
|
</gl-dropdown-item>
|
||||||
|
</template>
|
||||||
|
</gl-dropdown>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,426 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
GlAlert,
|
||||||
|
GlButton,
|
||||||
|
GlCollapse,
|
||||||
|
GlFormCheckbox,
|
||||||
|
GlFormCombobox,
|
||||||
|
GlFormGroup,
|
||||||
|
GlFormSelect,
|
||||||
|
GlFormInput,
|
||||||
|
GlFormTextarea,
|
||||||
|
GlIcon,
|
||||||
|
GlLink,
|
||||||
|
GlModal,
|
||||||
|
GlSprintf,
|
||||||
|
} from '@gitlab/ui';
|
||||||
|
import { mapActions, mapState } from 'vuex';
|
||||||
|
import { getCookie, setCookie } from '~/lib/utils/common_utils';
|
||||||
|
import { __ } from '~/locale';
|
||||||
|
import Tracking from '~/tracking';
|
||||||
|
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
|
import { mapComputed } from '~/vuex_shared/bindings';
|
||||||
|
import {
|
||||||
|
AWS_TOKEN_CONSTANTS,
|
||||||
|
ADD_CI_VARIABLE_MODAL_ID,
|
||||||
|
AWS_TIP_DISMISSED_COOKIE_NAME,
|
||||||
|
AWS_TIP_MESSAGE,
|
||||||
|
CONTAINS_VARIABLE_REFERENCE_MESSAGE,
|
||||||
|
ENVIRONMENT_SCOPE_LINK_TITLE,
|
||||||
|
EVENT_LABEL,
|
||||||
|
EVENT_ACTION,
|
||||||
|
} from '../constants';
|
||||||
|
import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
|
||||||
|
import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens';
|
||||||
|
|
||||||
|
const trackingMixin = Tracking.mixin({ label: EVENT_LABEL });
|
||||||
|
|
||||||
|
export default {
|
||||||
|
modalId: ADD_CI_VARIABLE_MODAL_ID,
|
||||||
|
tokens: awsTokens,
|
||||||
|
tokenList: awsTokenList,
|
||||||
|
awsTipMessage: AWS_TIP_MESSAGE,
|
||||||
|
containsVariableReferenceMessage: CONTAINS_VARIABLE_REFERENCE_MESSAGE,
|
||||||
|
environmentScopeLinkTitle: ENVIRONMENT_SCOPE_LINK_TITLE,
|
||||||
|
components: {
|
||||||
|
CiEnvironmentsDropdown,
|
||||||
|
GlAlert,
|
||||||
|
GlButton,
|
||||||
|
GlCollapse,
|
||||||
|
GlFormCheckbox,
|
||||||
|
GlFormCombobox,
|
||||||
|
GlFormGroup,
|
||||||
|
GlFormSelect,
|
||||||
|
GlFormInput,
|
||||||
|
GlFormTextarea,
|
||||||
|
GlIcon,
|
||||||
|
GlLink,
|
||||||
|
GlModal,
|
||||||
|
GlSprintf,
|
||||||
|
},
|
||||||
|
mixins: [glFeatureFlagsMixin(), trackingMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isTipDismissed: getCookie(AWS_TIP_DISMISSED_COOKIE_NAME) === 'true',
|
||||||
|
validationErrorEventProperty: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState([
|
||||||
|
'projectId',
|
||||||
|
'environments',
|
||||||
|
'typeOptions',
|
||||||
|
'variable',
|
||||||
|
'variableBeingEdited',
|
||||||
|
'isGroup',
|
||||||
|
'maskableRegex',
|
||||||
|
'selectedEnvironment',
|
||||||
|
'isProtectedByDefault',
|
||||||
|
'awsLogoSvgPath',
|
||||||
|
'awsTipDeployLink',
|
||||||
|
'awsTipCommandsLink',
|
||||||
|
'awsTipLearnLink',
|
||||||
|
'containsVariableReferenceLink',
|
||||||
|
'protectedEnvironmentVariablesLink',
|
||||||
|
'maskedEnvironmentVariablesLink',
|
||||||
|
'environmentScopeLink',
|
||||||
|
]),
|
||||||
|
...mapComputed(
|
||||||
|
[
|
||||||
|
{ key: 'key', updateFn: 'updateVariableKey' },
|
||||||
|
{ key: 'secret_value', updateFn: 'updateVariableValue' },
|
||||||
|
{ key: 'variable_type', updateFn: 'updateVariableType' },
|
||||||
|
{ key: 'environment_scope', updateFn: 'setEnvironmentScope' },
|
||||||
|
{ key: 'protected_variable', updateFn: 'updateVariableProtected' },
|
||||||
|
{ key: 'masked', updateFn: 'updateVariableMasked' },
|
||||||
|
],
|
||||||
|
false,
|
||||||
|
'variable',
|
||||||
|
),
|
||||||
|
isTipVisible() {
|
||||||
|
return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key);
|
||||||
|
},
|
||||||
|
canSubmit() {
|
||||||
|
return (
|
||||||
|
this.variableValidationState &&
|
||||||
|
this.variable.key !== '' &&
|
||||||
|
this.variable.secret_value !== ''
|
||||||
|
);
|
||||||
|
},
|
||||||
|
canMask() {
|
||||||
|
const regex = RegExp(this.maskableRegex);
|
||||||
|
return regex.test(this.variable.secret_value);
|
||||||
|
},
|
||||||
|
containsVariableReference() {
|
||||||
|
const regex = /\$/;
|
||||||
|
return regex.test(this.variable.secret_value);
|
||||||
|
},
|
||||||
|
displayMaskedError() {
|
||||||
|
return !this.canMask && this.variable.masked;
|
||||||
|
},
|
||||||
|
maskedState() {
|
||||||
|
if (this.displayMaskedError) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
modalActionText() {
|
||||||
|
return this.variableBeingEdited ? __('Update variable') : __('Add variable');
|
||||||
|
},
|
||||||
|
maskedFeedback() {
|
||||||
|
return this.displayMaskedError ? __('This variable can not be masked.') : '';
|
||||||
|
},
|
||||||
|
tokenValidationFeedback() {
|
||||||
|
const tokenSpecificFeedback = this.$options.tokens?.[this.variable.key]?.invalidMessage;
|
||||||
|
if (!this.tokenValidationState && tokenSpecificFeedback) {
|
||||||
|
return tokenSpecificFeedback;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
tokenValidationState() {
|
||||||
|
const validator = this.$options.tokens?.[this.variable.key]?.validation;
|
||||||
|
|
||||||
|
if (validator) {
|
||||||
|
return validator(this.variable.secret_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
scopedVariablesAvailable() {
|
||||||
|
return !this.isGroup || this.glFeatures.groupScopedCiVariables;
|
||||||
|
},
|
||||||
|
variableValidationFeedback() {
|
||||||
|
return `${this.tokenValidationFeedback} ${this.maskedFeedback}`;
|
||||||
|
},
|
||||||
|
variableValidationState() {
|
||||||
|
return this.variable.secret_value === '' || (this.tokenValidationState && this.maskedState);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
variable: {
|
||||||
|
handler() {
|
||||||
|
this.trackVariableValidationErrors();
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions([
|
||||||
|
'addVariable',
|
||||||
|
'updateVariable',
|
||||||
|
'resetEditing',
|
||||||
|
'displayInputValue',
|
||||||
|
'clearModal',
|
||||||
|
'deleteVariable',
|
||||||
|
'setEnvironmentScope',
|
||||||
|
'addWildCardScope',
|
||||||
|
'resetSelectedEnvironment',
|
||||||
|
'setSelectedEnvironment',
|
||||||
|
'setVariableProtected',
|
||||||
|
]),
|
||||||
|
dismissTip() {
|
||||||
|
setCookie(AWS_TIP_DISMISSED_COOKIE_NAME, 'true', { expires: 90 });
|
||||||
|
this.isTipDismissed = true;
|
||||||
|
},
|
||||||
|
deleteVarAndClose() {
|
||||||
|
this.deleteVariable();
|
||||||
|
this.hideModal();
|
||||||
|
},
|
||||||
|
hideModal() {
|
||||||
|
this.$refs.modal.hide();
|
||||||
|
},
|
||||||
|
resetModalHandler() {
|
||||||
|
if (this.variableBeingEdited) {
|
||||||
|
this.resetEditing();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clearModal();
|
||||||
|
this.resetSelectedEnvironment();
|
||||||
|
this.resetValidationErrorEvents();
|
||||||
|
},
|
||||||
|
updateOrAddVariable() {
|
||||||
|
if (this.variableBeingEdited) {
|
||||||
|
this.updateVariable();
|
||||||
|
} else {
|
||||||
|
this.addVariable();
|
||||||
|
}
|
||||||
|
this.hideModal();
|
||||||
|
},
|
||||||
|
setVariableProtectedByDefault() {
|
||||||
|
if (this.isProtectedByDefault && !this.variableBeingEdited) {
|
||||||
|
this.setVariableProtected();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trackVariableValidationErrors() {
|
||||||
|
const property = this.getTrackingErrorProperty();
|
||||||
|
if (!this.validationErrorEventProperty && property) {
|
||||||
|
this.track(EVENT_ACTION, { property });
|
||||||
|
this.validationErrorEventProperty = property;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getTrackingErrorProperty() {
|
||||||
|
let property;
|
||||||
|
if (this.variable.secret_value?.length && !property) {
|
||||||
|
if (this.displayMaskedError && this.maskableRegex?.length) {
|
||||||
|
const supportedChars = this.maskableRegex.replace('^', '').replace(/{(\d,)}\$/, '');
|
||||||
|
const regex = new RegExp(supportedChars, 'g');
|
||||||
|
property = this.variable.secret_value.replace(regex, '');
|
||||||
|
}
|
||||||
|
if (this.containsVariableReference) {
|
||||||
|
property = '$';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return property;
|
||||||
|
},
|
||||||
|
resetValidationErrorEvents() {
|
||||||
|
this.validationErrorEventProperty = '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<gl-modal
|
||||||
|
ref="modal"
|
||||||
|
:modal-id="$options.modalId"
|
||||||
|
:title="modalActionText"
|
||||||
|
static
|
||||||
|
lazy
|
||||||
|
@hidden="resetModalHandler"
|
||||||
|
@shown="setVariableProtectedByDefault"
|
||||||
|
>
|
||||||
|
<form>
|
||||||
|
<gl-form-combobox
|
||||||
|
v-model="key"
|
||||||
|
:token-list="$options.tokenList"
|
||||||
|
:label-text="__('Key')"
|
||||||
|
data-qa-selector="ci_variable_key_field"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<gl-form-group
|
||||||
|
:label="__('Value')"
|
||||||
|
label-for="ci-variable-value"
|
||||||
|
:state="variableValidationState"
|
||||||
|
:invalid-feedback="variableValidationFeedback"
|
||||||
|
>
|
||||||
|
<gl-form-textarea
|
||||||
|
id="ci-variable-value"
|
||||||
|
ref="valueField"
|
||||||
|
v-model="secret_value"
|
||||||
|
:state="variableValidationState"
|
||||||
|
rows="3"
|
||||||
|
max-rows="6"
|
||||||
|
data-qa-selector="ci_variable_value_field"
|
||||||
|
class="gl-font-monospace!"
|
||||||
|
/>
|
||||||
|
</gl-form-group>
|
||||||
|
|
||||||
|
<div class="d-flex">
|
||||||
|
<gl-form-group :label="__('Type')" label-for="ci-variable-type" class="w-50 gl-mr-5">
|
||||||
|
<gl-form-select id="ci-variable-type" v-model="variable_type" :options="typeOptions" />
|
||||||
|
</gl-form-group>
|
||||||
|
|
||||||
|
<gl-form-group label-for="ci-variable-env" class="w-50" data-testid="environment-scope">
|
||||||
|
<template #label>
|
||||||
|
{{ __('Environment scope') }}
|
||||||
|
<gl-link
|
||||||
|
:title="$options.environmentScopeLinkTitle"
|
||||||
|
:href="environmentScopeLink"
|
||||||
|
target="_blank"
|
||||||
|
data-testid="environment-scope-link"
|
||||||
|
>
|
||||||
|
<gl-icon name="question" :size="12" />
|
||||||
|
</gl-link>
|
||||||
|
</template>
|
||||||
|
<ci-environments-dropdown
|
||||||
|
v-if="scopedVariablesAvailable"
|
||||||
|
class="w-100"
|
||||||
|
:value="environment_scope"
|
||||||
|
@selectEnvironment="setEnvironmentScope"
|
||||||
|
@createClicked="addWildCardScope"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<gl-form-input v-else v-model="environment_scope" class="w-100" readonly />
|
||||||
|
</gl-form-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<gl-form-group :label="__('Flags')" label-for="ci-variable-flags">
|
||||||
|
<gl-form-checkbox
|
||||||
|
v-model="protected_variable"
|
||||||
|
class="mb-0"
|
||||||
|
data-testid="ci-variable-protected-checkbox"
|
||||||
|
>
|
||||||
|
{{ __('Protect variable') }}
|
||||||
|
<gl-link target="_blank" :href="protectedEnvironmentVariablesLink">
|
||||||
|
<gl-icon name="question" :size="12" />
|
||||||
|
</gl-link>
|
||||||
|
<p class="gl-mt-2 text-secondary">
|
||||||
|
{{ __('Export variable to pipelines running on protected branches and tags only.') }}
|
||||||
|
</p>
|
||||||
|
</gl-form-checkbox>
|
||||||
|
|
||||||
|
<gl-form-checkbox
|
||||||
|
ref="masked-ci-variable"
|
||||||
|
v-model="masked"
|
||||||
|
data-testid="ci-variable-masked-checkbox"
|
||||||
|
>
|
||||||
|
{{ __('Mask variable') }}
|
||||||
|
<gl-link target="_blank" :href="maskedEnvironmentVariablesLink">
|
||||||
|
<gl-icon name="question" :size="12" />
|
||||||
|
</gl-link>
|
||||||
|
<p class="gl-mt-2 gl-mb-0 text-secondary">
|
||||||
|
{{ __('Variable will be masked in job logs.') }}
|
||||||
|
<span
|
||||||
|
:class="{
|
||||||
|
'bold text-plain': displayMaskedError,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ __('Requires values to meet regular expression requirements.') }}</span
|
||||||
|
>
|
||||||
|
<gl-link target="_blank" :href="maskedEnvironmentVariablesLink">{{
|
||||||
|
__('More information')
|
||||||
|
}}</gl-link>
|
||||||
|
</p>
|
||||||
|
</gl-form-checkbox>
|
||||||
|
</gl-form-group>
|
||||||
|
</form>
|
||||||
|
<gl-collapse :visible="isTipVisible">
|
||||||
|
<gl-alert
|
||||||
|
:title="__('Deploying to AWS is easy with GitLab')"
|
||||||
|
variant="tip"
|
||||||
|
data-testid="aws-guidance-tip"
|
||||||
|
@dismiss="dismissTip"
|
||||||
|
>
|
||||||
|
<div class="gl-display-flex gl-flex-direction-row">
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<gl-sprintf :message="$options.awsTipMessage">
|
||||||
|
<template #deployLink="{ content }">
|
||||||
|
<gl-link :href="awsTipDeployLink" target="_blank">{{ content }}</gl-link>
|
||||||
|
</template>
|
||||||
|
<template #commandsLink="{ content }">
|
||||||
|
<gl-link :href="awsTipCommandsLink" target="_blank">{{ content }}</gl-link>
|
||||||
|
</template>
|
||||||
|
</gl-sprintf>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<gl-button
|
||||||
|
:href="awsTipLearnLink"
|
||||||
|
target="_blank"
|
||||||
|
category="secondary"
|
||||||
|
variant="info"
|
||||||
|
class="gl-overflow-wrap-break"
|
||||||
|
>{{ __('Learn more about deploying to AWS') }}</gl-button
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
class="gl-mt-3"
|
||||||
|
:alt="__('Amazon Web Services Logo')"
|
||||||
|
:src="awsLogoSvgPath"
|
||||||
|
height="32"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</gl-alert>
|
||||||
|
</gl-collapse>
|
||||||
|
<gl-alert
|
||||||
|
v-if="containsVariableReference"
|
||||||
|
:title="__('Value might contain a variable reference')"
|
||||||
|
:dismissible="false"
|
||||||
|
variant="warning"
|
||||||
|
data-testid="contains-variable-reference"
|
||||||
|
>
|
||||||
|
<gl-sprintf :message="$options.containsVariableReferenceMessage">
|
||||||
|
<template #code="{ content }">
|
||||||
|
<code>{{ content }}</code>
|
||||||
|
</template>
|
||||||
|
<template #docsLink="{ content }">
|
||||||
|
<gl-link :href="containsVariableReferenceLink" target="_blank">{{ content }}</gl-link>
|
||||||
|
</template>
|
||||||
|
</gl-sprintf>
|
||||||
|
</gl-alert>
|
||||||
|
<template #modal-footer>
|
||||||
|
<gl-button @click="hideModal">{{ __('Cancel') }}</gl-button>
|
||||||
|
<gl-button
|
||||||
|
v-if="variableBeingEdited"
|
||||||
|
ref="deleteCiVariable"
|
||||||
|
variant="danger"
|
||||||
|
category="secondary"
|
||||||
|
data-qa-selector="ci_variable_delete_button"
|
||||||
|
@click="deleteVarAndClose"
|
||||||
|
>{{ __('Delete variable') }}</gl-button
|
||||||
|
>
|
||||||
|
<gl-button
|
||||||
|
ref="updateOrAddVariable"
|
||||||
|
:disabled="!canSubmit"
|
||||||
|
variant="confirm"
|
||||||
|
category="primary"
|
||||||
|
data-testid="ciUpdateOrAddVariableBtn"
|
||||||
|
data-qa-selector="ci_variable_save_button"
|
||||||
|
@click="updateOrAddVariable"
|
||||||
|
>{{ modalActionText }}
|
||||||
|
</gl-button>
|
||||||
|
</template>
|
||||||
|
</gl-modal>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<script>
|
||||||
|
import { mapState, mapActions } from 'vuex';
|
||||||
|
import LegacyCiVariableModal from './legacy_ci_variable_modal.vue';
|
||||||
|
import LegacyCiVariableTable from './legacy_ci_variable_table.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
LegacyCiVariableModal,
|
||||||
|
LegacyCiVariableTable,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(['isGroup']),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (!this.isGroup) {
|
||||||
|
this.fetchEnvironments();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['fetchEnvironments']),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<legacy-ci-variable-table />
|
||||||
|
<legacy-ci-variable-modal />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
<script>
|
||||||
|
import { GlTable, GlButton, GlModalDirective, GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||||
|
import { mapState, mapActions } from 'vuex';
|
||||||
|
import { s__, __ } from '~/locale';
|
||||||
|
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
|
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
|
||||||
|
import { ADD_CI_VARIABLE_MODAL_ID } from '../constants';
|
||||||
|
import CiVariablePopover from './ci_variable_popover.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
modalId: ADD_CI_VARIABLE_MODAL_ID,
|
||||||
|
trueIcon: 'mobile-issue-close',
|
||||||
|
falseIcon: 'close',
|
||||||
|
iconSize: 16,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'variable_type',
|
||||||
|
label: s__('CiVariables|Type'),
|
||||||
|
customStyle: { width: '70px' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'key',
|
||||||
|
label: s__('CiVariables|Key'),
|
||||||
|
tdClass: 'text-plain',
|
||||||
|
sortable: true,
|
||||||
|
customStyle: { width: '40%' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'value',
|
||||||
|
label: s__('CiVariables|Value'),
|
||||||
|
customStyle: { width: '40%' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'protected',
|
||||||
|
label: s__('CiVariables|Protected'),
|
||||||
|
customStyle: { width: '100px' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'masked',
|
||||||
|
label: s__('CiVariables|Masked'),
|
||||||
|
customStyle: { width: '100px' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'environment_scope',
|
||||||
|
label: s__('CiVariables|Environments'),
|
||||||
|
customStyle: { width: '20%' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'actions',
|
||||||
|
label: '',
|
||||||
|
tdClass: 'text-right',
|
||||||
|
customStyle: { width: '35px' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
CiVariablePopover,
|
||||||
|
GlButton,
|
||||||
|
GlIcon,
|
||||||
|
GlTable,
|
||||||
|
TooltipOnTruncate,
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
GlModalDirective,
|
||||||
|
GlTooltip: GlTooltipDirective,
|
||||||
|
},
|
||||||
|
mixins: [glFeatureFlagsMixin()],
|
||||||
|
computed: {
|
||||||
|
...mapState(['variables', 'valuesHidden', 'isLoading', 'isDeleting']),
|
||||||
|
valuesButtonText() {
|
||||||
|
return this.valuesHidden ? __('Reveal values') : __('Hide values');
|
||||||
|
},
|
||||||
|
isTableEmpty() {
|
||||||
|
return !this.variables || this.variables.length === 0;
|
||||||
|
},
|
||||||
|
fields() {
|
||||||
|
return this.$options.fields;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchVariables();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['fetchVariables', 'toggleValues', 'editVariable']),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="ci-variable-table" data-testid="ci-variable-table">
|
||||||
|
<gl-table
|
||||||
|
:fields="fields"
|
||||||
|
:items="variables"
|
||||||
|
tbody-tr-class="js-ci-variable-row"
|
||||||
|
data-qa-selector="ci_variable_table_content"
|
||||||
|
sort-by="key"
|
||||||
|
sort-direction="asc"
|
||||||
|
stacked="lg"
|
||||||
|
table-class="text-secondary"
|
||||||
|
fixed
|
||||||
|
show-empty
|
||||||
|
sort-icon-left
|
||||||
|
no-sort-reset
|
||||||
|
>
|
||||||
|
<template #table-colgroup="scope">
|
||||||
|
<col v-for="field in scope.fields" :key="field.key" :style="field.customStyle" />
|
||||||
|
</template>
|
||||||
|
<template #cell(key)="{ item }">
|
||||||
|
<div class="gl-display-flex gl-align-items-center">
|
||||||
|
<tooltip-on-truncate :title="item.key" truncate-target="child">
|
||||||
|
<span
|
||||||
|
:id="`ci-variable-key-${item.id}`"
|
||||||
|
class="gl-display-inline-block gl-max-w-full gl-text-truncate"
|
||||||
|
>{{ item.key }}</span
|
||||||
|
>
|
||||||
|
</tooltip-on-truncate>
|
||||||
|
<gl-button
|
||||||
|
v-gl-tooltip
|
||||||
|
category="tertiary"
|
||||||
|
icon="copy-to-clipboard"
|
||||||
|
:title="__('Copy key')"
|
||||||
|
:data-clipboard-text="item.key"
|
||||||
|
:aria-label="__('Copy to clipboard')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #cell(value)="{ item }">
|
||||||
|
<div class="gl-display-flex gl-align-items-center">
|
||||||
|
<span v-if="valuesHidden">*********************</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
:id="`ci-variable-value-${item.id}`"
|
||||||
|
class="gl-display-inline-block gl-max-w-full gl-text-truncate"
|
||||||
|
>{{ item.value }}</span
|
||||||
|
>
|
||||||
|
<gl-button
|
||||||
|
v-gl-tooltip
|
||||||
|
category="tertiary"
|
||||||
|
icon="copy-to-clipboard"
|
||||||
|
:title="__('Copy value')"
|
||||||
|
:data-clipboard-text="item.value"
|
||||||
|
:aria-label="__('Copy to clipboard')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #cell(protected)="{ item }">
|
||||||
|
<gl-icon v-if="item.protected" :size="$options.iconSize" :name="$options.trueIcon" />
|
||||||
|
<gl-icon v-else :size="$options.iconSize" :name="$options.falseIcon" />
|
||||||
|
</template>
|
||||||
|
<template #cell(masked)="{ item }">
|
||||||
|
<gl-icon v-if="item.masked" :size="$options.iconSize" :name="$options.trueIcon" />
|
||||||
|
<gl-icon v-else :size="$options.iconSize" :name="$options.falseIcon" />
|
||||||
|
</template>
|
||||||
|
<template #cell(environment_scope)="{ item }">
|
||||||
|
<div class="gl-display-flex">
|
||||||
|
<span
|
||||||
|
:id="`ci-variable-env-${item.id}`"
|
||||||
|
class="gl-display-inline-block gl-max-w-full gl-text-truncate"
|
||||||
|
>{{ item.environment_scope }}</span
|
||||||
|
>
|
||||||
|
<ci-variable-popover
|
||||||
|
:target="`ci-variable-env-${item.id}`"
|
||||||
|
:value="item.environment_scope"
|
||||||
|
:tooltip-text="__('Copy environment')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #cell(actions)="{ item }">
|
||||||
|
<gl-button
|
||||||
|
v-gl-modal-directive="$options.modalId"
|
||||||
|
icon="pencil"
|
||||||
|
:aria-label="__('Edit')"
|
||||||
|
data-qa-selector="edit_ci_variable_button"
|
||||||
|
@click="editVariable(item)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #empty>
|
||||||
|
<p class="gl-text-center gl-py-6 gl-text-black-normal gl-mb-0">
|
||||||
|
{{ __('There are no variables yet.') }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</gl-table>
|
||||||
|
<div class="ci-variable-actions gl-display-flex gl-mt-5">
|
||||||
|
<gl-button
|
||||||
|
v-gl-modal-directive="$options.modalId"
|
||||||
|
class="gl-mr-3"
|
||||||
|
data-qa-selector="add_ci_variable_button"
|
||||||
|
variant="confirm"
|
||||||
|
category="primary"
|
||||||
|
>{{ __('Add variable') }}</gl-button
|
||||||
|
>
|
||||||
|
<gl-button
|
||||||
|
v-if="!isTableEmpty"
|
||||||
|
data-qa-selector="reveal_ci_variable_value_button"
|
||||||
|
@click="toggleValues(!valuesHidden)"
|
||||||
|
>{{ valuesButtonText }}</gl-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -1,9 +1,62 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import VueApollo from 'vue-apollo';
|
||||||
|
import createDefaultClient from '~/lib/graphql';
|
||||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||||
import CiVariableSettings from './components/ci_variable_settings.vue';
|
import CiVariableSettings from './components/ci_variable_settings.vue';
|
||||||
|
import LegacyCiVariableSettings from './components/legacy_ci_variable_settings.vue';
|
||||||
import createStore from './store';
|
import createStore from './store';
|
||||||
|
|
||||||
const mountCiVariableListApp = (containerEl) => {
|
const mountCiVariableListApp = (containerEl) => {
|
||||||
|
const {
|
||||||
|
awsLogoSvgPath,
|
||||||
|
awsTipCommandsLink,
|
||||||
|
awsTipDeployLink,
|
||||||
|
awsTipLearnLink,
|
||||||
|
containsVariableReferenceLink,
|
||||||
|
environmentScopeLink,
|
||||||
|
group,
|
||||||
|
maskedEnvironmentVariablesLink,
|
||||||
|
maskableRegex,
|
||||||
|
projectFullPath,
|
||||||
|
projectId,
|
||||||
|
protectedByDefault,
|
||||||
|
protectedEnvironmentVariablesLink,
|
||||||
|
} = containerEl.dataset;
|
||||||
|
|
||||||
|
const isGroup = parseBoolean(group);
|
||||||
|
const isProtectedByDefault = parseBoolean(protectedByDefault);
|
||||||
|
|
||||||
|
Vue.use(VueApollo);
|
||||||
|
|
||||||
|
const apolloProvider = new VueApollo({
|
||||||
|
defaultClient: createDefaultClient(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Vue({
|
||||||
|
el: containerEl,
|
||||||
|
apolloProvider,
|
||||||
|
provide: {
|
||||||
|
awsLogoSvgPath,
|
||||||
|
awsTipCommandsLink,
|
||||||
|
awsTipDeployLink,
|
||||||
|
awsTipLearnLink,
|
||||||
|
containsVariableReferenceLink,
|
||||||
|
environmentScopeLink,
|
||||||
|
isGroup,
|
||||||
|
isProtectedByDefault,
|
||||||
|
maskedEnvironmentVariablesLink,
|
||||||
|
maskableRegex,
|
||||||
|
projectFullPath,
|
||||||
|
projectId,
|
||||||
|
protectedEnvironmentVariablesLink,
|
||||||
|
},
|
||||||
|
render(createElement) {
|
||||||
|
return createElement(CiVariableSettings);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const mountLegacyCiVariableListApp = (containerEl) => {
|
||||||
const {
|
const {
|
||||||
endpoint,
|
endpoint,
|
||||||
projectId,
|
projectId,
|
||||||
|
|
@ -42,7 +95,7 @@ const mountCiVariableListApp = (containerEl) => {
|
||||||
el: containerEl,
|
el: containerEl,
|
||||||
store,
|
store,
|
||||||
render(createElement) {
|
render(createElement) {
|
||||||
return createElement(CiVariableSettings);
|
return createElement(LegacyCiVariableSettings);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -50,5 +103,11 @@ const mountCiVariableListApp = (containerEl) => {
|
||||||
export default (containerId = 'js-ci-project-variables') => {
|
export default (containerId = 'js-ci-project-variables') => {
|
||||||
const el = document.getElementById(containerId);
|
const el = document.getElementById(containerId);
|
||||||
|
|
||||||
return !el ? {} : mountCiVariableListApp(el);
|
if (el) {
|
||||||
|
if (gon.features?.ciVariableSettingsGraphql) {
|
||||||
|
mountCiVariableListApp(el);
|
||||||
|
} else {
|
||||||
|
mountLegacyCiVariableListApp(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
mutation addItems($items: [Item]) {
|
mutation addItems($items: [ItemInput]) {
|
||||||
addToolbarItems(items: $items) @client
|
addToolbarItems(items: $items) @client
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,16 @@ type Item {
|
||||||
selectedLabel: String
|
selectedLabel: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input ItemInput {
|
||||||
|
id: ID!
|
||||||
|
label: String!
|
||||||
|
icon: String
|
||||||
|
selected: Boolean
|
||||||
|
group: Int!
|
||||||
|
category: String
|
||||||
|
selectedLabel: String
|
||||||
|
}
|
||||||
|
|
||||||
type Items {
|
type Items {
|
||||||
nodes: [Item]!
|
nodes: [Item]!
|
||||||
}
|
}
|
||||||
|
|
@ -17,7 +27,7 @@ extend type Query {
|
||||||
}
|
}
|
||||||
|
|
||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
updateToolbarItem(id: ID!, propsToUpdate: Item!): LocalErrors
|
updateToolbarItem(id: ID!, propsToUpdate: ItemInput!): LocalErrors
|
||||||
removeToolbarItems(ids: [ID!]): LocalErrors
|
removeToolbarItems(ids: [ID!]): LocalErrors
|
||||||
addToolbarItems(items: [Item]): LocalErrors
|
addToolbarItems(items: [ItemInput]): LocalErrors
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
mutation updateItem($id: ID!, $propsToUpdate: Item!) {
|
mutation updateItem($id: ID!, $propsToUpdate: ItemInput!) {
|
||||||
updateToolbarItem(id: $id, propsToUpdate: $propsToUpdate) @client
|
updateToolbarItem(id: $id, propsToUpdate: $propsToUpdate) @client
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
mutation action($action: LocalAction) {
|
mutation action($action: LocalActionInput) {
|
||||||
action(action: $action) @client {
|
action(action: $action) @client {
|
||||||
errors
|
errors
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,11 @@ type LocalEnvironment {
|
||||||
autoStopPath: String
|
autoStopPath: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input LocalActionInput {
|
||||||
|
name: String!
|
||||||
|
playPath: String
|
||||||
|
}
|
||||||
|
|
||||||
input LocalEnvironmentInput {
|
input LocalEnvironmentInput {
|
||||||
id: Int!
|
id: Int!
|
||||||
globalId: ID!
|
globalId: ID!
|
||||||
|
|
@ -64,7 +69,7 @@ type LocalPageInfo {
|
||||||
|
|
||||||
extend type Query {
|
extend type Query {
|
||||||
environmentApp(page: Int, scope: String): LocalEnvironmentApp
|
environmentApp(page: Int, scope: String): LocalEnvironmentApp
|
||||||
folder(environment: NestedLocalEnvironmentInput): LocalEnvironmentFolder
|
folder(environment: NestedLocalEnvironmentInput, scope: String): LocalEnvironmentFolder
|
||||||
environmentToDelete: LocalEnvironment
|
environmentToDelete: LocalEnvironment
|
||||||
pageInfo: LocalPageInfo
|
pageInfo: LocalPageInfo
|
||||||
environmentToRollback: LocalEnvironment
|
environmentToRollback: LocalEnvironment
|
||||||
|
|
@ -82,5 +87,5 @@ extend type Mutation {
|
||||||
setEnvironmentToRollback(environment: LocalEnvironmentInput): LocalErrors
|
setEnvironmentToRollback(environment: LocalEnvironmentInput): LocalErrors
|
||||||
setEnvironmentToStop(environment: LocalEnvironmentInput): LocalErrors
|
setEnvironmentToStop(environment: LocalEnvironmentInput): LocalErrors
|
||||||
setEnvironmentToChangeCanary(environment: LocalEnvironmentInput, weight: Int): LocalErrors
|
setEnvironmentToChangeCanary(environment: LocalEnvironmentInput, weight: Int): LocalErrors
|
||||||
action(environment: LocalEnvironmentInput): LocalErrors
|
action(action: LocalActionInput): LocalErrors
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
mutation importGroups($importRequests: [ImportGroupInput!]!) {
|
mutation importGroups($importRequests: [ImportRequestInput!]!) {
|
||||||
importGroups(importRequests: $importRequests) @client {
|
importGroups(importRequests: $importRequests) @client {
|
||||||
id
|
id
|
||||||
lastImportTarget {
|
lastImportTarget {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#import "ee_else_ce/repository/queries/commit.fragment.graphql"
|
#import "ee_else_ce/repository/queries/commit.fragment.graphql"
|
||||||
|
|
||||||
query getCommit($fileName: String!, $type: String!, $path: String!, $maxOffset: Number!) {
|
query getCommit($fileName: String!, $type: String!, $path: String!, $maxOffset: Int!) {
|
||||||
commit(path: $path, fileName: $fileName, type: $type, maxOffset: $maxOffset) @client {
|
commit(path: $path, fileName: $fileName, type: $type, maxOffset: $maxOffset) @client {
|
||||||
...TreeEntryCommit
|
...TreeEntryCommit
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
type LogTreeCommit {
|
||||||
|
sha: String
|
||||||
|
message: String
|
||||||
|
titleHtml: String
|
||||||
|
committedDate: Time
|
||||||
|
commitPath: String
|
||||||
|
fileName: String
|
||||||
|
filePath: String
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
mutation addDataToTerraformState($terraformState: State!) {
|
mutation addDataToTerraformState($terraformState: LocalTerraformStateInput!) {
|
||||||
addDataToTerraformState(terraformState: $terraformState) @client
|
addDataToTerraformState(terraformState: $terraformState) @client
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
extend type TerraformState {
|
||||||
|
_showDetails: Boolean
|
||||||
|
errorMessages: [String]
|
||||||
|
loadingLock: Boolean
|
||||||
|
loadingRemove: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
input LocalTerraformStateInput {
|
||||||
|
_showDetails: Boolean
|
||||||
|
errorMessages: [String]
|
||||||
|
loadingLock: Boolean
|
||||||
|
loadingRemove: Boolean
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
lockedAt: Time
|
||||||
|
updatedAt: Time!
|
||||||
|
deletedAt: Time
|
||||||
|
}
|
||||||
|
|
||||||
|
extend type Mutation {
|
||||||
|
addDataToTerraformState(terraformState: LocalTerraformStateInput!): Boolean
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,7 @@ extend type WorkItem {
|
||||||
mockWidgets: [LocalWorkItemWidget]
|
mockWidgets: [LocalWorkItemWidget]
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocalWorkItemAssigneesInput {
|
input LocalWorkItemAssigneesInput {
|
||||||
id: WorkItemID!
|
id: WorkItemID!
|
||||||
assigneeIds: [ID!]
|
assigneeIds: [ID!]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
@import './pages/note_form';
|
@import './pages/note_form';
|
||||||
@import './pages/notes';
|
@import './pages/notes';
|
||||||
@import './pages/notifications';
|
@import './pages/notifications';
|
||||||
@import './pages/pages';
|
|
||||||
@import './pages/pipelines';
|
@import './pages/pipelines';
|
||||||
@import './pages/profile';
|
@import './pages/profile';
|
||||||
@import './pages/profiles/preferences';
|
@import './pages/profiles/preferences';
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@
|
||||||
@import 'bootstrap/scss/buttons';
|
@import 'bootstrap/scss/buttons';
|
||||||
@import 'bootstrap/scss/forms';
|
@import 'bootstrap/scss/forms';
|
||||||
|
|
||||||
|
@import '@gitlab/ui/src/scss/variables';
|
||||||
|
@import '@gitlab/ui/src/scss/utility-mixins/index';
|
||||||
|
@import '@gitlab/ui/src/components/base/button/button';
|
||||||
|
|
||||||
$body-color: #666;
|
$body-color: #666;
|
||||||
$header-color: #456;
|
$header-color: #456;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
.pages-domain-list {
|
|
||||||
&-item {
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.domain-status {
|
|
||||||
display: inline-flex;
|
|
||||||
left: $gl-padding;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.domain-name {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
&.has-verification-status > li {
|
|
||||||
padding-left: 3 * $gl-padding;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge {
|
|
||||||
|
|
||||||
display: inline-flex;
|
|
||||||
margin-bottom: $gl-padding-8;
|
|
||||||
|
|
||||||
// Most of the following settings "stolen" from btn-sm
|
|
||||||
// Border radius is overwritten for both
|
|
||||||
.label,
|
|
||||||
.btn {
|
|
||||||
padding: $gl-padding-4 $gl-padding-8;
|
|
||||||
font-size: $gl-font-size;
|
|
||||||
line-height: $gl-btn-line-height;
|
|
||||||
border-radius: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn svg {
|
|
||||||
top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:first-child {
|
|
||||||
line-height: $gl-line-height;
|
|
||||||
}
|
|
||||||
|
|
||||||
:not(:first-child) {
|
|
||||||
border-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:last-child {
|
|
||||||
border-radius: $border-radius-default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -13,6 +13,7 @@ module Projects
|
||||||
before_action :define_variables
|
before_action :define_variables
|
||||||
before_action do
|
before_action do
|
||||||
push_frontend_feature_flag(:ajax_new_deploy_token, @project)
|
push_frontend_feature_flag(:ajax_new_deploy_token, @project)
|
||||||
|
push_frontend_feature_flag(:ci_variable_settings_graphql, @project)
|
||||||
end
|
end
|
||||||
|
|
||||||
helper_method :highlight_badge
|
helper_method :highlight_badge
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ module InviteMembersHelper
|
||||||
invalid_groups: source.related_group_ids,
|
invalid_groups: source.related_group_ids,
|
||||||
help_link: help_page_url('user/permissions'),
|
help_link: help_page_url('user/permissions'),
|
||||||
is_project: is_project,
|
is_project: is_project,
|
||||||
access_levels: member_class.access_level_roles.to_json
|
access_levels: member_class.permissible_access_level_roles(current_user, source).to_json
|
||||||
}.merge(group_select_data(source))
|
}.merge(group_select_data(source))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,12 +52,6 @@ module ProjectsHelper
|
||||||
content_tag(:span, username, name_tag_options)
|
content_tag(:span, username, name_tag_options)
|
||||||
end
|
end
|
||||||
|
|
||||||
def permissible_access_level_roles(current_user, project)
|
|
||||||
# Access level roles that the current user is able to grant others.
|
|
||||||
# This method is a stopgap in preparation for https://gitlab.com/gitlab-org/gitlab/-/issues/364087
|
|
||||||
current_user.can?(:manage_owners, project) ? Gitlab::Access.options_with_owner : Gitlab::Access.options
|
|
||||||
end
|
|
||||||
|
|
||||||
def link_to_member(project, author, opts = {}, &block)
|
def link_to_member(project, author, opts = {}, &block)
|
||||||
default_opts = { avatar: true, name: true, title: ":name" }
|
default_opts = { avatar: true, name: true, title: ":name" }
|
||||||
opts = default_opts.merge(opts)
|
opts = default_opts.merge(opts)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ module Clusters
|
||||||
|
|
||||||
scope :ordered_by_name, -> { order(:name) }
|
scope :ordered_by_name, -> { order(:name) }
|
||||||
scope :with_name, -> (name) { where(name: name) }
|
scope :with_name, -> (name) { where(name: name) }
|
||||||
|
scope :has_vulnerabilities, -> (value = true) { where(has_vulnerabilities: value) }
|
||||||
|
|
||||||
validates :name,
|
validates :name,
|
||||||
presence: true,
|
presence: true,
|
||||||
|
|
|
||||||
|
|
@ -362,7 +362,7 @@ class Group < Namespace
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_users(users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
|
def add_users(users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
|
||||||
Members::Groups::BulkCreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
|
Members::Groups::CreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
|
||||||
self,
|
self,
|
||||||
users,
|
users,
|
||||||
access_level,
|
access_level,
|
||||||
|
|
@ -374,7 +374,7 @@ class Group < Namespace
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_user(user, access_level, current_user: nil, expires_at: nil, ldap: false, blocking_refresh: true)
|
def add_user(user, access_level, current_user: nil, expires_at: nil, ldap: false, blocking_refresh: true)
|
||||||
Members::Groups::CreatorService.new( # rubocop:disable CodeReuse/ServiceClass
|
Members::Groups::CreatorService.add_user( # rubocop:disable CodeReuse/ServiceClass
|
||||||
self,
|
self,
|
||||||
user,
|
user,
|
||||||
access_level,
|
access_level,
|
||||||
|
|
@ -382,7 +382,7 @@ class Group < Namespace
|
||||||
expires_at: expires_at,
|
expires_at: expires_at,
|
||||||
ldap: ldap,
|
ldap: ldap,
|
||||||
blocking_refresh: blocking_refresh
|
blocking_refresh: blocking_refresh
|
||||||
).execute
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_guest(user, current_user = nil)
|
def add_guest(user, current_user = nil)
|
||||||
|
|
|
||||||
|
|
@ -31,11 +31,6 @@ class ProjectHook < WebHook
|
||||||
_('Webhooks')
|
_('Webhooks')
|
||||||
end
|
end
|
||||||
|
|
||||||
override :rate_limit
|
|
||||||
def rate_limit
|
|
||||||
project.actual_limits.limit_for(:web_hook_calls)
|
|
||||||
end
|
|
||||||
|
|
||||||
override :application_context
|
override :application_context
|
||||||
def application_context
|
def application_context
|
||||||
super.merge(project: project)
|
super.merge(project: project)
|
||||||
|
|
|
||||||
|
|
@ -127,19 +127,12 @@ class WebHook < ApplicationRecord
|
||||||
|
|
||||||
# @return [Boolean] Whether or not the WebHook is currently throttled.
|
# @return [Boolean] Whether or not the WebHook is currently throttled.
|
||||||
def rate_limited?
|
def rate_limited?
|
||||||
return false unless rate_limit
|
rate_limiter.rate_limited?
|
||||||
|
|
||||||
Gitlab::ApplicationRateLimiter.peek(
|
|
||||||
:web_hook_calls,
|
|
||||||
scope: [self],
|
|
||||||
threshold: rate_limit
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Threshold for the rate-limit.
|
# @return [Integer] The rate limit for the WebHook. `0` for no limit.
|
||||||
# Overridden in ProjectHook and GroupHook, other WebHooks are not rate-limited.
|
|
||||||
def rate_limit
|
def rate_limit
|
||||||
nil
|
rate_limiter.limit
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns the associated Project or Group for the WebHook if one exists.
|
# Returns the associated Project or Group for the WebHook if one exists.
|
||||||
|
|
@ -180,4 +173,8 @@ class WebHook < ApplicationRecord
|
||||||
def initialize_url_variables
|
def initialize_url_variables
|
||||||
self.url_variables = {} if encrypted_url_variables.nil?
|
self.url_variables = {} if encrypted_url_variables.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def rate_limiter
|
||||||
|
@rate_limiter ||= Gitlab::WebHooks::RateLimiter.new(self)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,12 @@ class GroupMember < Member
|
||||||
|
|
||||||
attr_accessor :last_owner, :last_blocked_owner
|
attr_accessor :last_owner, :last_blocked_owner
|
||||||
|
|
||||||
|
# For those who get to see a modal with a role dropdown, here are the options presented
|
||||||
|
def self.permissible_access_level_roles(_, _)
|
||||||
|
# This method is a stopgap in preparation for https://gitlab.com/gitlab-org/gitlab/-/issues/364087
|
||||||
|
access_level_roles
|
||||||
|
end
|
||||||
|
|
||||||
def self.access_level_roles
|
def self.access_level_roles
|
||||||
Gitlab::Access.options_with_owner
|
Gitlab::Access.options_with_owner
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ class ProjectMember < Member
|
||||||
project_ids.each do |project_id|
|
project_ids.each do |project_id|
|
||||||
project = Project.find(project_id)
|
project = Project.find(project_id)
|
||||||
|
|
||||||
Members::Projects::BulkCreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
|
Members::Projects::CreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
|
||||||
project,
|
project,
|
||||||
users,
|
users,
|
||||||
access_level,
|
access_level,
|
||||||
|
|
@ -73,6 +73,16 @@ class ProjectMember < Member
|
||||||
truncate_teams [project.id]
|
truncate_teams [project.id]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# For those who get to see a modal with a role dropdown, here are the options presented
|
||||||
|
def permissible_access_level_roles(current_user, project)
|
||||||
|
# This method is a stopgap in preparation for https://gitlab.com/gitlab-org/gitlab/-/issues/364087
|
||||||
|
if Ability.allowed?(current_user, :manage_owners, project)
|
||||||
|
Gitlab::Access.options_with_owner
|
||||||
|
else
|
||||||
|
ProjectMember.access_level_roles
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def access_level_roles
|
def access_level_roles
|
||||||
Gitlab::Access.options
|
Gitlab::Access.options
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ class ProjectTeam
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_users(users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
|
def add_users(users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
|
||||||
Members::Projects::BulkCreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
|
Members::Projects::CreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
|
||||||
project,
|
project,
|
||||||
users,
|
users,
|
||||||
access_level,
|
access_level,
|
||||||
|
|
@ -56,12 +56,12 @@ class ProjectTeam
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_user(user, access_level, current_user: nil, expires_at: nil)
|
def add_user(user, access_level, current_user: nil, expires_at: nil)
|
||||||
Members::Projects::CreatorService.new(project, # rubocop:disable CodeReuse/ServiceClass
|
Members::Projects::CreatorService.add_user( # rubocop:disable CodeReuse/ServiceClass
|
||||||
|
project,
|
||||||
user,
|
user,
|
||||||
access_level,
|
access_level,
|
||||||
current_user: current_user,
|
current_user: current_user,
|
||||||
expires_at: expires_at)
|
expires_at: expires_at)
|
||||||
.execute
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Remove all users from project team
|
# Remove all users from project team
|
||||||
|
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Members
|
|
||||||
module BulkCreateUsers
|
|
||||||
extend ActiveSupport::Concern
|
|
||||||
|
|
||||||
included do
|
|
||||||
class << self
|
|
||||||
def add_users(source, users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
|
|
||||||
return [] unless users.present?
|
|
||||||
|
|
||||||
# If this user is attempting to manage Owner members and doesn't have permission, do not allow
|
|
||||||
return [] if managing_owners?(current_user, access_level) && cannot_manage_owners?(source, current_user)
|
|
||||||
|
|
||||||
emails, users, existing_members = parse_users_list(source, users)
|
|
||||||
|
|
||||||
Member.transaction do
|
|
||||||
(emails + users).map! do |user|
|
|
||||||
new(source,
|
|
||||||
user,
|
|
||||||
access_level,
|
|
||||||
existing_members: existing_members,
|
|
||||||
current_user: current_user,
|
|
||||||
expires_at: expires_at,
|
|
||||||
tasks_to_be_done: tasks_to_be_done,
|
|
||||||
tasks_project_id: tasks_project_id)
|
|
||||||
.execute
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def managing_owners?(current_user, access_level)
|
|
||||||
current_user && Gitlab::Access.sym_options_with_owner[access_level] == Gitlab::Access::OWNER
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse_users_list(source, list)
|
|
||||||
emails = []
|
|
||||||
user_ids = []
|
|
||||||
users = []
|
|
||||||
existing_members = {}
|
|
||||||
|
|
||||||
list.each do |item|
|
|
||||||
case item
|
|
||||||
when User
|
|
||||||
users << item
|
|
||||||
when Integer
|
|
||||||
user_ids << item
|
|
||||||
when /\A\d+\Z/
|
|
||||||
user_ids << item.to_i
|
|
||||||
when Devise.email_regexp
|
|
||||||
emails << item
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# the below will automatically discard invalid user_ids
|
|
||||||
users.concat(User.id_in(user_ids)) if user_ids.present?
|
|
||||||
users.uniq! # de-duplicate just in case as there is no controlling if user records and ids are sent multiple times
|
|
||||||
|
|
||||||
users_by_emails = source.users_by_emails(emails) # preloads our request store for all emails
|
|
||||||
# in case emails belong to a user that is being invited by user or user_id, remove them from
|
|
||||||
# emails and let users/user_ids handle it.
|
|
||||||
parsed_emails = emails.select do |email|
|
|
||||||
user = users_by_emails[email]
|
|
||||||
!user || (users.exclude?(user) && user_ids.exclude?(user.id))
|
|
||||||
end
|
|
||||||
|
|
||||||
if users.present?
|
|
||||||
# helps not have to perform another query per user id to see if the member exists later on when fetching
|
|
||||||
existing_members = source.members_and_requesters.with_user(users).index_by(&:user_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
[parsed_emails, users, existing_members]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(source, user, access_level, **args)
|
|
||||||
super
|
|
||||||
|
|
||||||
@existing_members = args[:existing_members] || (raise ArgumentError, "existing_members must be included in the args hash")
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
attr_reader :existing_members
|
|
||||||
|
|
||||||
def find_or_initialize_member_by_user
|
|
||||||
existing_members[user.id] || source.members.build(user_id: user.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -5,13 +5,18 @@ module JiraConnectSubscriptions
|
||||||
include Gitlab::Utils::StrongMemoize
|
include Gitlab::Utils::StrongMemoize
|
||||||
MERGE_REQUEST_SYNC_BATCH_SIZE = 20
|
MERGE_REQUEST_SYNC_BATCH_SIZE = 20
|
||||||
MERGE_REQUEST_SYNC_BATCH_DELAY = 1.minute.freeze
|
MERGE_REQUEST_SYNC_BATCH_DELAY = 1.minute.freeze
|
||||||
NOT_SITE_ADMIN = 'The Jira user is not a site administrator.'
|
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
return error(NOT_SITE_ADMIN, 403) unless can_administer_jira?
|
if !params[:jira_user]
|
||||||
|
return error(s_('JiraConnect|Could not fetch user information from Jira. ' \
|
||||||
|
'Check the permissions in Jira and try again.'), 403)
|
||||||
|
elsif !can_administer_jira?
|
||||||
|
return error(s_('JiraConnect|The Jira user is not a site administrator. ' \
|
||||||
|
'Check the permissions in Jira and try again.'), 403)
|
||||||
|
end
|
||||||
|
|
||||||
unless namespace && can?(current_user, :create_jira_connect_subscription, namespace)
|
unless namespace && can?(current_user, :create_jira_connect_subscription, namespace)
|
||||||
return error('Invalid namespace. Please make sure you have sufficient permissions', 401)
|
return error(s_('JiraConnect|Cannot find namespace. Make sure you have sufficient permissions.'), 401)
|
||||||
end
|
end
|
||||||
|
|
||||||
create_subscription
|
create_subscription
|
||||||
|
|
@ -20,7 +25,7 @@ module JiraConnectSubscriptions
|
||||||
private
|
private
|
||||||
|
|
||||||
def can_administer_jira?
|
def can_administer_jira?
|
||||||
@params[:jira_user]&.site_admin?
|
params[:jira_user]&.site_admin?
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_subscription
|
def create_subscription
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,105 @@ module Members
|
||||||
def access_levels
|
def access_levels
|
||||||
Gitlab::Access.sym_options_with_owner
|
Gitlab::Access.sym_options_with_owner
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def add_users( # rubocop:disable Metrics/ParameterLists
|
||||||
|
source,
|
||||||
|
users,
|
||||||
|
access_level,
|
||||||
|
current_user: nil,
|
||||||
|
expires_at: nil,
|
||||||
|
tasks_to_be_done: [],
|
||||||
|
tasks_project_id: nil,
|
||||||
|
ldap: nil,
|
||||||
|
blocking_refresh: nil
|
||||||
|
)
|
||||||
|
return [] unless users.present?
|
||||||
|
|
||||||
|
# If this user is attempting to manage Owner members and doesn't have permission, do not allow
|
||||||
|
return [] if managing_owners?(current_user, access_level) && cannot_manage_owners?(source, current_user)
|
||||||
|
|
||||||
|
emails, users, existing_members = parse_users_list(source, users)
|
||||||
|
|
||||||
|
Member.transaction do
|
||||||
|
(emails + users).map! do |user|
|
||||||
|
new(source,
|
||||||
|
user,
|
||||||
|
access_level,
|
||||||
|
existing_members: existing_members,
|
||||||
|
current_user: current_user,
|
||||||
|
expires_at: expires_at,
|
||||||
|
tasks_to_be_done: tasks_to_be_done,
|
||||||
|
tasks_project_id: tasks_project_id,
|
||||||
|
ldap: ldap,
|
||||||
|
blocking_refresh: blocking_refresh)
|
||||||
|
.execute
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_user( # rubocop:disable Metrics/ParameterLists
|
||||||
|
source,
|
||||||
|
user,
|
||||||
|
access_level,
|
||||||
|
current_user: nil,
|
||||||
|
expires_at: nil,
|
||||||
|
ldap: nil,
|
||||||
|
blocking_refresh: nil
|
||||||
|
)
|
||||||
|
add_users(source,
|
||||||
|
[user],
|
||||||
|
access_level,
|
||||||
|
current_user: current_user,
|
||||||
|
expires_at: expires_at,
|
||||||
|
ldap: ldap,
|
||||||
|
blocking_refresh: blocking_refresh).first
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def managing_owners?(current_user, access_level)
|
||||||
|
current_user && Gitlab::Access.sym_options_with_owner[access_level] == Gitlab::Access::OWNER
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_users_list(source, list)
|
||||||
|
emails = []
|
||||||
|
user_ids = []
|
||||||
|
users = []
|
||||||
|
existing_members = {}
|
||||||
|
|
||||||
|
list.each do |item|
|
||||||
|
case item
|
||||||
|
when User
|
||||||
|
users << item
|
||||||
|
when Integer
|
||||||
|
user_ids << item
|
||||||
|
when /\A\d+\Z/
|
||||||
|
user_ids << item.to_i
|
||||||
|
when Devise.email_regexp
|
||||||
|
emails << item
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# the below will automatically discard invalid user_ids
|
||||||
|
users.concat(User.id_in(user_ids)) if user_ids.present?
|
||||||
|
# de-duplicate just in case as there is no controlling if user records and ids are sent multiple times
|
||||||
|
users.uniq!
|
||||||
|
|
||||||
|
users_by_emails = source.users_by_emails(emails) # preloads our request store for all emails
|
||||||
|
# in case emails belong to a user that is being invited by user or user_id, remove them from
|
||||||
|
# emails and let users/user_ids handle it.
|
||||||
|
parsed_emails = emails.select do |email|
|
||||||
|
user = users_by_emails[email]
|
||||||
|
!user || (users.exclude?(user) && user_ids.exclude?(user.id))
|
||||||
|
end
|
||||||
|
|
||||||
|
if users.present? || users_by_emails.present?
|
||||||
|
# helps not have to perform another query per user id to see if the member exists later on when fetching
|
||||||
|
existing_members = source.members_and_requesters.with_user(users + users_by_emails.values).index_by(&:user_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
[parsed_emails, users, existing_members]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(source, user, access_level, **args)
|
def initialize(source, user, access_level, **args)
|
||||||
|
|
@ -21,10 +120,12 @@ module Members
|
||||||
@args = args
|
@args = args
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private_class_method :new
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
find_or_build_member
|
find_or_build_member
|
||||||
commit_member
|
commit_member
|
||||||
create_member_task
|
after_commit_tasks
|
||||||
|
|
||||||
member
|
member
|
||||||
end
|
end
|
||||||
|
|
@ -92,6 +193,10 @@ module Members
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def after_commit_tasks
|
||||||
|
create_member_task
|
||||||
|
end
|
||||||
|
|
||||||
def create_member_task
|
def create_member_task
|
||||||
return unless member.persisted?
|
return unless member.persisted?
|
||||||
return if member_task_attributes.value?(nil)
|
return if member_task_attributes.value?(nil)
|
||||||
|
|
@ -163,15 +268,19 @@ module Members
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_initialize_member_by_user
|
def find_or_initialize_member_by_user
|
||||||
# have to use members and requesters here since project/group limits on requested_at being nil for members and
|
# We have to use `members_and_requesters` here since the given `members` is modified in the models
|
||||||
# wouldn't be found in `source.members` if it already existed
|
# to act more like a scope(removing the requested_at members) and therefore ActiveRecord has issues with that
|
||||||
# this of course will not treat active invites the same since we aren't searching on email
|
# on build and refreshing that relation.
|
||||||
source.members_and_requesters.find_or_initialize_by(user_id: user.id) # rubocop:disable CodeReuse/ActiveRecord
|
existing_members[user.id] || source.members_and_requesters.build(user_id: user.id) # rubocop:disable CodeReuse/ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def ldap
|
def ldap
|
||||||
args[:ldap] || false
|
args[:ldap] || false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def existing_members
|
||||||
|
args[:existing_members] || {}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Members
|
|
||||||
module Groups
|
|
||||||
class BulkCreatorService < Members::Groups::CreatorService
|
|
||||||
include Members::BulkCreateUsers
|
|
||||||
|
|
||||||
class << self
|
|
||||||
def cannot_manage_owners?(source, current_user)
|
|
||||||
source.max_member_access_for_user(current_user) < Gitlab::Access::OWNER
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -3,6 +3,12 @@
|
||||||
module Members
|
module Members
|
||||||
module Groups
|
module Groups
|
||||||
class CreatorService < Members::CreatorService
|
class CreatorService < Members::CreatorService
|
||||||
|
class << self
|
||||||
|
def cannot_manage_owners?(source, current_user)
|
||||||
|
source.max_member_access_for_user(current_user) < Gitlab::Access::OWNER
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def can_create_new_member?
|
def can_create_new_member?
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Members
|
|
||||||
module Projects
|
|
||||||
class BulkCreatorService < Members::Projects::CreatorService
|
|
||||||
include Members::BulkCreateUsers
|
|
||||||
|
|
||||||
class << self
|
|
||||||
def cannot_manage_owners?(source, current_user)
|
|
||||||
!Ability.allowed?(current_user, :manage_owners, source)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -3,6 +3,12 @@
|
||||||
module Members
|
module Members
|
||||||
module Projects
|
module Projects
|
||||||
class CreatorService < Members::CreatorService
|
class CreatorService < Members::CreatorService
|
||||||
|
class << self
|
||||||
|
def cannot_manage_owners?(source, current_user)
|
||||||
|
!Ability.allowed?(current_user, :manage_owners, source)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def can_create_new_member?
|
def can_create_new_member?
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ class WebHookService
|
||||||
|
|
||||||
def async_execute
|
def async_execute
|
||||||
Gitlab::ApplicationContext.with_context(hook.application_context) do
|
Gitlab::ApplicationContext.with_context(hook.application_context) do
|
||||||
break log_rate_limited if rate_limited?
|
break log_rate_limited if rate_limit!
|
||||||
break log_recursion_blocked if recursion_blocked?
|
break log_recursion_blocked if recursion_blocked?
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
|
|
@ -215,24 +215,16 @@ class WebHookService
|
||||||
string_size_limit(response_body, RESPONSE_BODY_SIZE_LIMIT)
|
string_size_limit(response_body, RESPONSE_BODY_SIZE_LIMIT)
|
||||||
end
|
end
|
||||||
|
|
||||||
def rate_limited?
|
# Increments rate-limit counter.
|
||||||
return false if rate_limit.nil?
|
# Returns true if hook should be rate-limited.
|
||||||
|
def rate_limit!
|
||||||
Gitlab::ApplicationRateLimiter.throttled?(
|
Gitlab::WebHooks::RateLimiter.new(hook).rate_limit!
|
||||||
:web_hook_calls,
|
|
||||||
scope: [hook],
|
|
||||||
threshold: rate_limit
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def recursion_blocked?
|
def recursion_blocked?
|
||||||
Gitlab::WebHooks::RecursionDetection.block?(hook)
|
Gitlab::WebHooks::RecursionDetection.block?(hook)
|
||||||
end
|
end
|
||||||
|
|
||||||
def rate_limit
|
|
||||||
@rate_limit ||= hook.rate_limit
|
|
||||||
end
|
|
||||||
|
|
||||||
def log_rate_limited
|
def log_rate_limited
|
||||||
log_auth_error('Webhook rate limit exceeded')
|
log_auth_error('Webhook rate limit exceeded')
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,6 @@
|
||||||
%p
|
%p
|
||||||
= s_('403|Please contact your GitLab administrator to get permission.')
|
= s_('403|Please contact your GitLab administrator to get permission.')
|
||||||
.action-container.js-go-back{ hidden: true }
|
.action-container.js-go-back{ hidden: true }
|
||||||
%button{ type: 'button', class: 'gl-button btn btn-success' }
|
= render Pajamas::ButtonComponent.new(variant: :confirm) do
|
||||||
= _('Go Back')
|
= _('Go Back')
|
||||||
= render "errors/footer"
|
= render "errors/footer"
|
||||||
|
|
|
||||||
|
|
@ -52,5 +52,4 @@
|
||||||
.settings-content
|
.settings-content
|
||||||
= render 'groups/settings/ci_cd/auto_devops_form', group: @group
|
= render 'groups/settings/ci_cd/auto_devops_form', group: @group
|
||||||
|
|
||||||
- if ::Feature.enabled?(:group_level_protected_environment, @group)
|
|
||||||
= render_if_exists 'groups/settings/ci_cd/protected_environments', expanded: expanded
|
= render_if_exists 'groups/settings/ci_cd/protected_environments', expanded: expanded
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
- return unless can_admin_project_member?(project)
|
- return unless can_admin_project_member?(project)
|
||||||
|
|
||||||
.js-invite-members-modal{ data: { is_project: 'true',
|
.js-invite-members-modal{ data: { is_project: 'true',
|
||||||
access_levels: ProjectMember.access_level_roles.to_json,
|
access_levels: ProjectMember.permissible_access_level_roles(current_user, project).to_json,
|
||||||
help_link: help_page_url('user/permissions') }.merge(common_invite_modal_dataset(project)).merge(users_filter_data(project.group)) }
|
help_link: help_page_url('user/permissions') }.merge(common_invite_modal_dataset(project)).merge(users_filter_data(project.group)) }
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,13 @@
|
||||||
.card
|
.card
|
||||||
.card-header
|
.card-header
|
||||||
Domains (#{@domains.size})
|
Domains (#{@domains.size})
|
||||||
%ul.list-group.list-group-flush.pages-domain-list{ class: ("has-verification-status" if verification_enabled) }
|
%ul.list-group.list-group-flush
|
||||||
- @domains.each do |domain|
|
- @domains.each do |domain|
|
||||||
%li.pages-domain-list-item.list-group-item.d-flex.justify-content-between
|
%li.list-group-item.gl-display-flex.gl-justify-content-space-between.gl-align-items-center
|
||||||
|
.gl-display-flex.gl-align-items-center
|
||||||
- if verification_enabled
|
- if verification_enabled
|
||||||
- tooltip, status = domain.unverified? ? [s_('GitLabPages|Unverified'), 'failed'] : [s_('GitLabPages|Verified'), 'success']
|
- tooltip, status = domain.unverified? ? [s_('GitLabPages|Unverified'), 'failed'] : [s_('GitLabPages|Verified'), 'success']
|
||||||
.domain-status.ci-status-icon.has-tooltip{ class: "ci-status-icon-#{status}", title: tooltip }
|
.domain-status.ci-status-icon.has-tooltip{ class: "gl-mr-5 ci-status-icon-#{status}", title: tooltip }
|
||||||
= sprite_icon("status_#{status}" )
|
= sprite_icon("status_#{status}" )
|
||||||
.domain-name
|
.domain-name
|
||||||
= external_link(domain.url, domain.url)
|
= external_link(domain.url, domain.url)
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,10 @@
|
||||||
.col-sm-2
|
.col-sm-2
|
||||||
= _("Verification status")
|
= _("Verification status")
|
||||||
.col-sm-10
|
.col-sm-10
|
||||||
.status-badge
|
.gl-mb-3
|
||||||
- text, status = domain_presenter.unverified? ? [_('Unverified'), :danger] : [_('Verified'), :success]
|
- text, status = domain_presenter.unverified? ? [_('Unverified'), :danger] : [_('Verified'), :success]
|
||||||
= gl_badge_tag text, variant: status
|
= gl_badge_tag text, variant: status
|
||||||
= link_to sprite_icon("redo"), verify_project_pages_domain_path(@project, domain_presenter), method: :post, class: "gl-ml-2 gl-button btn btn-default has-tooltip", title: _("Retry verification")
|
= link_to sprite_icon("redo"), verify_project_pages_domain_path(@project, domain_presenter), method: :post, class: "gl-ml-2 gl-button btn btn-sm btn-default has-tooltip", title: _("Retry verification")
|
||||||
.input-group
|
.input-group
|
||||||
= text_field_tag :domain_verification, verification_record, class: "monospace js-select-on-focus form-control", readonly: true
|
= text_field_tag :domain_verification, verification_record, class: "monospace js-select-on-focus form-control", readonly: true
|
||||||
.input-group-append
|
.input-group-append
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
resource: @project,
|
resource: @project,
|
||||||
token: @resource_access_token,
|
token: @resource_access_token,
|
||||||
scopes: @scopes,
|
scopes: @scopes,
|
||||||
access_levels: permissible_access_level_roles(current_user, @project),
|
access_levels: ProjectMember.permissible_access_level_roles(current_user, @project),
|
||||||
default_access_level: Gitlab::Access::MAINTAINER,
|
default_access_level: Gitlab::Access::MAINTAINER,
|
||||||
prefix: :resource_access_token,
|
prefix: :resource_access_token,
|
||||||
help_path: help_page_path('user/project/settings/project_access_tokens', anchor: 'scopes-for-a-project-access-token')
|
help_path: help_page_path('user/project/settings/project_access_tokens', anchor: 'scopes-for-a-project-access-token')
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
name: group_level_protected_environment
|
name: ci_variable_settings_graphql
|
||||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88506
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89332
|
||||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/363450
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/364423
|
||||||
milestone: '15.1'
|
milestone: '15.1'
|
||||||
type: development
|
type: development
|
||||||
group: group::release
|
group: group::pipeline authoring
|
||||||
default_enabled: false
|
default_enabled: false
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddWebHookCallsMedAndMaxToPlanLimits < Gitlab::Database::Migration[2.0]
|
||||||
|
def change
|
||||||
|
add_column :plan_limits, :web_hook_calls_mid, :integer, null: false, default: 0
|
||||||
|
add_column :plan_limits, :web_hook_calls_low, :integer, null: false, default: 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddWebHookCallsToPlanLimitsPaidTiers < Gitlab::Database::Migration[2.0]
|
||||||
|
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||||
|
|
||||||
|
MAX_RATE_LIMIT_NAME = 'web_hook_calls'
|
||||||
|
MID_RATE_LIMIT_NAME = 'web_hook_calls_mid'
|
||||||
|
MIN_RATE_LIMIT_NAME = 'web_hook_calls_low'
|
||||||
|
|
||||||
|
UP_FREE_LIMITS = {
|
||||||
|
MAX_RATE_LIMIT_NAME => 500,
|
||||||
|
MID_RATE_LIMIT_NAME => 500,
|
||||||
|
MIN_RATE_LIMIT_NAME => 500
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
UP_PREMIUM_LIMITS = {
|
||||||
|
MAX_RATE_LIMIT_NAME => 4_000,
|
||||||
|
MID_RATE_LIMIT_NAME => 2_800,
|
||||||
|
MIN_RATE_LIMIT_NAME => 1_600
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
UP_ULTIMATE_LIMITS = {
|
||||||
|
MAX_RATE_LIMIT_NAME => 13_000,
|
||||||
|
MID_RATE_LIMIT_NAME => 9_000,
|
||||||
|
MIN_RATE_LIMIT_NAME => 6_000
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
DOWN_FREE_LIMITS = {
|
||||||
|
# 120 is the value for 'free' migrated in `db/migrate/20210601131742_update_web_hook_calls_limit.rb`
|
||||||
|
MAX_RATE_LIMIT_NAME => 120,
|
||||||
|
MID_RATE_LIMIT_NAME => 0,
|
||||||
|
MIN_RATE_LIMIT_NAME => 0
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
DOWN_PAID_LIMITS = {
|
||||||
|
MAX_RATE_LIMIT_NAME => 0,
|
||||||
|
MID_RATE_LIMIT_NAME => 0,
|
||||||
|
MIN_RATE_LIMIT_NAME => 0
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def up
|
||||||
|
return unless Gitlab.com?
|
||||||
|
|
||||||
|
apply_limits('free', UP_FREE_LIMITS)
|
||||||
|
|
||||||
|
# Apply Premium limits
|
||||||
|
apply_limits('bronze', UP_PREMIUM_LIMITS)
|
||||||
|
apply_limits('silver', UP_PREMIUM_LIMITS)
|
||||||
|
apply_limits('premium', UP_PREMIUM_LIMITS)
|
||||||
|
apply_limits('premium_trial', UP_PREMIUM_LIMITS)
|
||||||
|
|
||||||
|
# Apply Ultimate limits
|
||||||
|
apply_limits('gold', UP_ULTIMATE_LIMITS)
|
||||||
|
apply_limits('ultimate', UP_ULTIMATE_LIMITS)
|
||||||
|
apply_limits('ultimate_trial', UP_ULTIMATE_LIMITS)
|
||||||
|
apply_limits('opensource', UP_ULTIMATE_LIMITS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
return unless Gitlab.com?
|
||||||
|
|
||||||
|
apply_limits('free', DOWN_FREE_LIMITS)
|
||||||
|
|
||||||
|
apply_limits('bronze', DOWN_PAID_LIMITS)
|
||||||
|
apply_limits('silver', DOWN_PAID_LIMITS)
|
||||||
|
apply_limits('premium', DOWN_PAID_LIMITS)
|
||||||
|
apply_limits('premium_trial', DOWN_PAID_LIMITS)
|
||||||
|
apply_limits('gold', DOWN_PAID_LIMITS)
|
||||||
|
apply_limits('ultimate', DOWN_PAID_LIMITS)
|
||||||
|
apply_limits('ultimate_trial', DOWN_PAID_LIMITS)
|
||||||
|
apply_limits('opensource', DOWN_PAID_LIMITS)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def apply_limits(plan_name, limits)
|
||||||
|
limits.each_pair do |limit_name, limit|
|
||||||
|
create_or_update_plan_limit(limit_name, plan_name, limit)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddTimestampsToComplianceFrameworks < Gitlab::Database::Migration[2.0]
|
||||||
|
def up
|
||||||
|
add_column :compliance_management_frameworks, :created_at, :datetime_with_timezone, null: true
|
||||||
|
add_column :compliance_management_frameworks, :updated_at, :datetime_with_timezone, null: true
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_column :compliance_management_frameworks, :created_at
|
||||||
|
remove_column :compliance_management_frameworks, :updated_at
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddHasVulnerabilitiesToClusterAgents < Gitlab::Database::Migration[2.0]
|
||||||
|
enable_lock_retries!
|
||||||
|
|
||||||
|
def change
|
||||||
|
add_column :cluster_agents, :has_vulnerabilities, :boolean, default: false, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddCreatedAtIndexToComplianceManagementFrameworks < Gitlab::Database::Migration[2.0]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
INDEX_NAME = "i_compliance_frameworks_on_id_and_created_at"
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_concurrent_index :compliance_management_frameworks,
|
||||||
|
[:id, :created_at, :pipeline_configuration_full_path],
|
||||||
|
name: INDEX_NAME
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_concurrent_index_by_name :compliance_management_frameworks, INDEX_NAME
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddIndexOnClustersAgentProjectIdAndHasVulnerabilitiesColumns < Gitlab::Database::Migration[2.0]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
INDEX_NAME = 'index_cluster_agents_on_project_id_and_has_vulnerabilities'
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_concurrent_index :cluster_agents,
|
||||||
|
[:project_id, :has_vulnerabilities],
|
||||||
|
name: INDEX_NAME
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_concurrent_index_by_name :cluster_agents, INDEX_NAME
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
80535374849c10d41663d339b95b9ffddbec9b40a8af4585c18602cbe92c14d1
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
92a7ed079521ccb8ab04e59826947778c37bccd30d47f1b0e29727f769e3ff32
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
f49e691c46ddaaf1b18d95726e7c2473fab946ea79885727ba09bb92591e4a01
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
96d899efc1fa39cf3433987ee4d8062456f7a6af6248b97eda2ddc5491dcf7f5
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
bbfcaf59734b67142b237b7ea479c5eaa3c2152cdd84c87ad541e5a0e75466ef
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
33456ce3af299e010011b1346b4097ffa1ee642ffb90d342ea22171c3f079d7a
|
||||||
|
|
@ -13346,6 +13346,7 @@ CREATE TABLE cluster_agents (
|
||||||
project_id bigint NOT NULL,
|
project_id bigint NOT NULL,
|
||||||
name text NOT NULL,
|
name text NOT NULL,
|
||||||
created_by_user_id bigint,
|
created_by_user_id bigint,
|
||||||
|
has_vulnerabilities boolean DEFAULT false NOT NULL,
|
||||||
CONSTRAINT check_3498369510 CHECK ((char_length(name) <= 255))
|
CONSTRAINT check_3498369510 CHECK ((char_length(name) <= 255))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -13795,6 +13796,8 @@ CREATE TABLE compliance_management_frameworks (
|
||||||
color text NOT NULL,
|
color text NOT NULL,
|
||||||
namespace_id integer NOT NULL,
|
namespace_id integer NOT NULL,
|
||||||
pipeline_configuration_full_path text,
|
pipeline_configuration_full_path text,
|
||||||
|
created_at timestamp with time zone,
|
||||||
|
updated_at timestamp with time zone,
|
||||||
CONSTRAINT check_08cd34b2c2 CHECK ((char_length(color) <= 10)),
|
CONSTRAINT check_08cd34b2c2 CHECK ((char_length(color) <= 10)),
|
||||||
CONSTRAINT check_1617e0b87e CHECK ((char_length(description) <= 255)),
|
CONSTRAINT check_1617e0b87e CHECK ((char_length(description) <= 255)),
|
||||||
CONSTRAINT check_ab00bc2193 CHECK ((char_length(name) <= 255)),
|
CONSTRAINT check_ab00bc2193 CHECK ((char_length(name) <= 255)),
|
||||||
|
|
@ -18780,7 +18783,9 @@ CREATE TABLE plan_limits (
|
||||||
pipeline_triggers integer DEFAULT 25000 NOT NULL,
|
pipeline_triggers integer DEFAULT 25000 NOT NULL,
|
||||||
project_ci_secure_files integer DEFAULT 100 NOT NULL,
|
project_ci_secure_files integer DEFAULT 100 NOT NULL,
|
||||||
repository_size bigint DEFAULT 0 NOT NULL,
|
repository_size bigint DEFAULT 0 NOT NULL,
|
||||||
security_policy_scan_execution_schedules integer DEFAULT 0 NOT NULL
|
security_policy_scan_execution_schedules integer DEFAULT 0 NOT NULL,
|
||||||
|
web_hook_calls_mid integer DEFAULT 0 NOT NULL,
|
||||||
|
web_hook_calls_low integer DEFAULT 0 NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE SEQUENCE plan_limits_id_seq
|
CREATE SEQUENCE plan_limits_id_seq
|
||||||
|
|
@ -26767,6 +26772,8 @@ CREATE INDEX i_batched_background_migration_job_transition_logs_on_job_id ON ONL
|
||||||
|
|
||||||
CREATE UNIQUE INDEX i_ci_job_token_project_scope_links_on_source_and_target_project ON ci_job_token_project_scope_links USING btree (source_project_id, target_project_id);
|
CREATE UNIQUE INDEX i_ci_job_token_project_scope_links_on_source_and_target_project ON ci_job_token_project_scope_links USING btree (source_project_id, target_project_id);
|
||||||
|
|
||||||
|
CREATE INDEX i_compliance_frameworks_on_id_and_created_at ON compliance_management_frameworks USING btree (id, created_at, pipeline_configuration_full_path);
|
||||||
|
|
||||||
CREATE INDEX idx_analytics_devops_adoption_segments_on_namespace_id ON analytics_devops_adoption_segments USING btree (namespace_id);
|
CREATE INDEX idx_analytics_devops_adoption_segments_on_namespace_id ON analytics_devops_adoption_segments USING btree (namespace_id);
|
||||||
|
|
||||||
CREATE INDEX idx_analytics_devops_adoption_snapshots_finalized ON analytics_devops_adoption_snapshots USING btree (namespace_id, end_time) WHERE (recorded_at >= end_time);
|
CREATE INDEX idx_analytics_devops_adoption_snapshots_finalized ON analytics_devops_adoption_snapshots USING btree (namespace_id, end_time) WHERE (recorded_at >= end_time);
|
||||||
|
|
@ -27543,6 +27550,8 @@ CREATE UNIQUE INDEX index_cluster_agent_tokens_on_token_encrypted ON cluster_age
|
||||||
|
|
||||||
CREATE INDEX index_cluster_agents_on_created_by_user_id ON cluster_agents USING btree (created_by_user_id);
|
CREATE INDEX index_cluster_agents_on_created_by_user_id ON cluster_agents USING btree (created_by_user_id);
|
||||||
|
|
||||||
|
CREATE INDEX index_cluster_agents_on_project_id_and_has_vulnerabilities ON cluster_agents USING btree (project_id, has_vulnerabilities);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX index_cluster_agents_on_project_id_and_name ON cluster_agents USING btree (project_id, name);
|
CREATE UNIQUE INDEX index_cluster_agents_on_project_id_and_name ON cluster_agents USING btree (project_id, name);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX index_cluster_enabled_grants_on_namespace_id ON cluster_enabled_grants USING btree (namespace_id);
|
CREATE UNIQUE INDEX index_cluster_enabled_grants_on_namespace_id ON cluster_enabled_grants USING btree (namespace_id);
|
||||||
|
|
|
||||||
|
|
@ -133,8 +133,9 @@ Limit the maximum daily member invitations allowed per group hierarchy.
|
||||||
|
|
||||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61151) in GitLab 13.12.
|
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61151) in GitLab 13.12.
|
||||||
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/330133) in GitLab 14.1.
|
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/330133) in GitLab 14.1.
|
||||||
|
> - [Limit changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89591) from per-hook to per-top-level namespace in GitLab 15.1.
|
||||||
|
|
||||||
Limit the number of times any given webhook can be called per minute.
|
Limit the number of times a webhook can be called per minute, per top-level namespace.
|
||||||
This only applies to project and group webhooks.
|
This only applies to project and group webhooks.
|
||||||
|
|
||||||
Calls over the rate limit are logged into `auth.log`.
|
Calls over the rate limit are logged into `auth.log`.
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,9 @@ levels are defined in the `Gitlab::Access` module. Currently, these levels are v
|
||||||
- Maintainer (`40`)
|
- Maintainer (`40`)
|
||||||
- Owner (`50`) - Only valid to set for groups
|
- Owner (`50`) - Only valid to set for groups
|
||||||
|
|
||||||
WARNING:
|
NOTE:
|
||||||
Due to [an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/219299),
|
From [GitLab 14.9](https://gitlab.com/gitlab-org/gitlab/-/issues/351211) and later, projects have a maximum role of Owner.
|
||||||
projects in personal namespaces don't show owner (`50`) permission.
|
Because of a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/219299) in GitLab 14.8 and earlier, projects have a maximum role of Maintainer.
|
||||||
|
|
||||||
## Add a member to a group or project
|
## Add a member to a group or project
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,7 @@ To protect a group-level environment, make sure your environments have the corre
|
||||||
|
|
||||||
#### Using the UI
|
#### Using the UI
|
||||||
|
|
||||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/325249) in GitLab 15.1 with a flag named `group_level_protected_environment`. Disabled by default.
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/325249) in GitLab 15.1.
|
||||||
|
|
||||||
1. On the top bar, select **Menu > Groups** and find your group.
|
1. On the top bar, select **Menu > Groups** and find your group.
|
||||||
1. On the left sidebar, select **Settings > CI/CD**.
|
1. On the left sidebar, select **Settings > CI/CD**.
|
||||||
|
|
|
||||||
|
|
@ -407,6 +407,10 @@ The requirements are the same as the previous settings:
|
||||||
} }
|
} }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Group Sync
|
||||||
|
|
||||||
|
For information on automatically managing GitLab group membership, see [SAML Group Sync](../user/group/saml_sso/group_sync.md).
|
||||||
|
|
||||||
## Bypass two factor authentication
|
## Bypass two factor authentication
|
||||||
|
|
||||||
If you want some SAML authentication methods to count as 2FA on a per session
|
If you want some SAML authentication methods to count as 2FA on a per session
|
||||||
|
|
|
||||||
|
|
@ -234,7 +234,7 @@ The following limits apply for [webhooks](../project/integrations/webhooks.md):
|
||||||
|
|
||||||
| Setting | Default for GitLab.com |
|
| Setting | Default for GitLab.com |
|
||||||
|----------------------|-------------------------|
|
|----------------------|-------------------------|
|
||||||
| Webhook rate limit | `120` calls per minute for GitLab Free, unlimited for GitLab Premium and GitLab Ultimate |
|
| Webhook rate limit | `500` calls per minute for GitLab Free, unlimited for GitLab Premium and GitLab Ultimate. Webhook rate limits are applied per top-level namespace. |
|
||||||
| Number of webhooks | `100` per project, `50` per group |
|
| Number of webhooks | `100` per project, `50` per group |
|
||||||
| Maximum payload size | 25 MB |
|
| Maximum payload size | 25 MB |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
---
|
||||||
|
type: reference, howto
|
||||||
|
stage: Manage
|
||||||
|
group: Authentication and Authorization
|
||||||
|
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||||
|
---
|
||||||
|
|
||||||
|
# SAML Group Sync **(PREMIUM)**
|
||||||
|
|
||||||
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/363084) for self-managed instances in GitLab 15.1.
|
||||||
|
|
||||||
|
WARNING:
|
||||||
|
Changing Group Sync configuration can remove users from the mapped GitLab group.
|
||||||
|
Removal happens if there is any mismatch between the group names and the list of `groups` in the SAML response.
|
||||||
|
If changes must be made, ensure either the SAML response includes the `groups` attribute
|
||||||
|
and the `AttributeValue` value matches the **SAML Group Name** in GitLab,
|
||||||
|
or that all groups are removed from GitLab to disable Group Sync.
|
||||||
|
|
||||||
|
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
|
||||||
|
For a demo of Group Sync using Azure, see [Demo: SAML Group Sync](https://youtu.be/Iqvo2tJfXjg).
|
||||||
|
|
||||||
|
## Configure SAML Group Sync
|
||||||
|
|
||||||
|
To configure SAML Group Sync:
|
||||||
|
|
||||||
|
1. Configure SAML authentication:
|
||||||
|
- For GitLab self-managed, see [SAML OmniAuth Provider](../../../integration/saml.md).
|
||||||
|
- For GitLab.com, see [SAML SSO for GitLab.com groups](index.md).
|
||||||
|
1. Ensure your SAML identity provider sends an attribute statement named `Groups` or `groups`.
|
||||||
|
|
||||||
|
NOTE:
|
||||||
|
The value for `Groups` or `groups` in the SAML response can be either the group name or the group ID.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<saml:AttributeStatement>
|
||||||
|
<saml:Attribute Name="Groups">
|
||||||
|
<saml:AttributeValue xsi:type="xs:string">Developers</saml:AttributeValue>
|
||||||
|
<saml:AttributeValue xsi:type="xs:string">Product Managers</saml:AttributeValue>
|
||||||
|
</saml:Attribute>
|
||||||
|
</saml:AttributeStatement>
|
||||||
|
```
|
||||||
|
|
||||||
|
Other attribute names such as `http://schemas.microsoft.com/ws/2008/06/identity/claims/groups`
|
||||||
|
are not accepted as a source of groups.
|
||||||
|
See the [SAML troubleshooting page](../../../administration/troubleshooting/group_saml_scim.md)
|
||||||
|
for examples on configuring the required attribute name in the SAML identity provider's settings.
|
||||||
|
|
||||||
|
## Configure SAML Group Links
|
||||||
|
|
||||||
|
When SAML is enabled, users with the Maintainer or Owner role
|
||||||
|
see a new menu item in group **Settings > SAML Group Links**. You can configure one or more **SAML Group Links** to map
|
||||||
|
a SAML identity provider group name to a GitLab role. This can be done for a top-level group or any subgroup.
|
||||||
|
|
||||||
|
To link the SAML groups:
|
||||||
|
|
||||||
|
1. In **SAML Group Name**, enter the value of the relevant `saml:AttributeValue`.
|
||||||
|
1. Choose the role in **Access Level**.
|
||||||
|
1. Select **Save**.
|
||||||
|
1. Repeat to add additional group links if required.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
If a user is a member of multiple SAML groups mapped to the same GitLab group,
|
||||||
|
the user gets the highest role from the groups. For example, if one group
|
||||||
|
is linked as Guest and another Maintainer, a user in both groups gets the Maintainer
|
||||||
|
role.
|
||||||
|
|
||||||
|
Users granted:
|
||||||
|
|
||||||
|
- A higher role with Group Sync are displayed as having
|
||||||
|
[direct membership](../../project/members/#display-direct-members) of the group.
|
||||||
|
- A lower or the same role with Group Sync are displayed as having
|
||||||
|
[inherited membership](../../project/members/#display-inherited-members) of the group.
|
||||||
|
|
||||||
|
### Automatic member removal
|
||||||
|
|
||||||
|
After a group sync, for GitLab subgroups, users who are not members of a mapped SAML
|
||||||
|
group are removed from the group.
|
||||||
|
|
||||||
|
FLAG:
|
||||||
|
In [GitLab 15.1 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/364144), on GitLab.com, users in the top-level
|
||||||
|
group are assigned the [default membership role](index.md#role) rather than removed. This setting is enabled with the
|
||||||
|
`saml_group_sync_retain_default_membership` feature flag and can be configured by GitLab.com administrators only.
|
||||||
|
|
||||||
|
For example, in the following diagram:
|
||||||
|
|
||||||
|
- Alex Garcia signs into GitLab and is removed from GitLab Group C because they don't belong
|
||||||
|
to SAML Group C.
|
||||||
|
- Sidney Jones belongs to SAML Group C, but is not added to GitLab Group C because they have
|
||||||
|
not yet signed in.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph SAML users
|
||||||
|
SAMLUserA[Sidney Jones]
|
||||||
|
SAMLUserB[Zhang Wei]
|
||||||
|
SAMLUserC[Alex Garcia]
|
||||||
|
SAMLUserD[Charlie Smith]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph SAML groups
|
||||||
|
SAMLGroupA["Group A"] --> SAMLGroupB["Group B"]
|
||||||
|
SAMLGroupA --> SAMLGroupC["Group C"]
|
||||||
|
SAMLGroupA --> SAMLGroupD["Group D"]
|
||||||
|
end
|
||||||
|
|
||||||
|
SAMLGroupB --> |Member|SAMLUserA
|
||||||
|
SAMLGroupB --> |Member|SAMLUserB
|
||||||
|
|
||||||
|
SAMLGroupC --> |Member|SAMLUserA
|
||||||
|
SAMLGroupC --> |Member|SAMLUserB
|
||||||
|
|
||||||
|
SAMLGroupD --> |Member|SAMLUserD
|
||||||
|
SAMLGroupD --> |Member|SAMLUserC
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph GitLab users
|
||||||
|
GitLabUserA[Sidney Jones]
|
||||||
|
GitLabUserB[Zhang Wei]
|
||||||
|
GitLabUserC[Alex Garcia]
|
||||||
|
GitLabUserD[Charlie Smith]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph GitLab groups
|
||||||
|
GitLabGroupA["Group A (SAML configured)"] --> GitLabGroupB["Group B (SAML Group Link not configured)"]
|
||||||
|
GitLabGroupA --> GitLabGroupC["Group C (SAML Group Link configured)"]
|
||||||
|
GitLabGroupA --> GitLabGroupD["Group D (SAML Group Link configured)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
GitLabGroupB --> |Member|GitLabUserA
|
||||||
|
|
||||||
|
GitLabGroupC --> |Member|GitLabUserB
|
||||||
|
GitLabGroupC --> |Member|GitLabUserC
|
||||||
|
|
||||||
|
GitLabGroupD --> |Member|GitLabUserC
|
||||||
|
GitLabGroupD --> |Member|GitLabUserD
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph GitLab users
|
||||||
|
GitLabUserA[Sidney Jones]
|
||||||
|
GitLabUserB[Zhang Wei]
|
||||||
|
GitLabUserC[Alex Garcia]
|
||||||
|
GitLabUserD[Charlie Smith]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph GitLab groups after Alex Garcia signs in
|
||||||
|
GitLabGroupA[Group A]
|
||||||
|
GitLabGroupA["Group A (SAML configured)"] --> GitLabGroupB["Group B (SAML Group Link not configured)"]
|
||||||
|
GitLabGroupA --> GitLabGroupC["Group C (SAML Group Link configured)"]
|
||||||
|
GitLabGroupA --> GitLabGroupD["Group D (SAML Group Link configured)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
GitLabGroupB --> |Member|GitLabUserA
|
||||||
|
GitLabGroupC --> |Member|GitLabUserB
|
||||||
|
GitLabGroupD --> |Member|GitLabUserC
|
||||||
|
GitLabGroupD --> |Member|GitLabUserD
|
||||||
|
```
|
||||||
|
|
@ -372,7 +372,7 @@ To rescind a user's access to the group when only SAML SSO is configured, either
|
||||||
- Remove (in order) the user from:
|
- Remove (in order) the user from:
|
||||||
1. The user data store on the identity provider or the list of users on the specific app.
|
1. The user data store on the identity provider or the list of users on the specific app.
|
||||||
1. The GitLab.com group.
|
1. The GitLab.com group.
|
||||||
- Use Group Sync at the top-level of your group to [automatically remove the user](#automatic-member-removal).
|
- Use Group Sync at the top-level of your group to [automatically remove the user](group_sync.md#automatic-member-removal).
|
||||||
|
|
||||||
To rescind a user's access to the group when also using SCIM, refer to [Blocking access](scim_setup.md#blocking-access).
|
To rescind a user's access to the group when also using SCIM, refer to [Blocking access](scim_setup.md#blocking-access).
|
||||||
|
|
||||||
|
|
@ -402,151 +402,7 @@ For example, to unlink the `MyOrg` account:
|
||||||
|
|
||||||
## Group Sync
|
## Group Sync
|
||||||
|
|
||||||
WARNING:
|
For information on automatically managing GitLab group membership, see [SAML Group Sync](group_sync.md).
|
||||||
Changing Group Sync configuration can remove users from the relevant GitLab group.
|
|
||||||
Removal happens if there is any mismatch between the group names and the list of `groups` in the SAML response.
|
|
||||||
If changes must be made, ensure either the SAML response includes the `groups` attribute
|
|
||||||
and the `AttributeValue` value matches the **SAML Group Name** in GitLab,
|
|
||||||
or that all groups are removed from GitLab to disable Group Sync.
|
|
||||||
|
|
||||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
|
|
||||||
For a demo of Group Sync using Azure, see [Demo: SAML Group Sync](https://youtu.be/Iqvo2tJfXjg).
|
|
||||||
|
|
||||||
When the SAML response includes a user and their group memberships from the SAML identity provider,
|
|
||||||
GitLab uses that information to automatically manage that user's GitLab group memberships.
|
|
||||||
|
|
||||||
Ensure your SAML identity provider sends an attribute statement named `Groups` or `groups` like the following:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<saml:AttributeStatement>
|
|
||||||
<saml:Attribute Name="Groups">
|
|
||||||
<saml:AttributeValue xsi:type="xs:string">Developers</saml:AttributeValue>
|
|
||||||
<saml:AttributeValue xsi:type="xs:string">Product Managers</saml:AttributeValue>
|
|
||||||
</saml:Attribute>
|
|
||||||
</saml:AttributeStatement>
|
|
||||||
```
|
|
||||||
|
|
||||||
Other attribute names such as `http://schemas.microsoft.com/ws/2008/06/identity/claims/groups`
|
|
||||||
are not accepted as a source of groups.
|
|
||||||
See the [SAML troubleshooting page](../../../administration/troubleshooting/group_saml_scim.md)
|
|
||||||
for examples on configuring the required attribute name in the SAML identity provider's settings.
|
|
||||||
|
|
||||||
NOTE:
|
|
||||||
The value for `Groups` or `groups` in the SAML response can be either the group name or the group ID.
|
|
||||||
To inspect the SAML response, you can use one of these [SAML debugging tools](#saml-debugging-tools).
|
|
||||||
|
|
||||||
When SAML SSO is enabled for the top-level group, `Maintainer` and `Owner` level users
|
|
||||||
see a new menu item in group **Settings > SAML Group Links**. You can configure one or more **SAML Group Links** to map
|
|
||||||
a SAML identity provider group name to a GitLab Access Level. This can be done for the parent group or the subgroups.
|
|
||||||
|
|
||||||
To link the SAML groups from the `saml:AttributeStatement` example above:
|
|
||||||
|
|
||||||
1. In the **SAML Group Name** box, enter the value of `saml:AttributeValue`.
|
|
||||||
1. Choose the desired **Access Level**.
|
|
||||||
1. **Save** the group link.
|
|
||||||
1. Repeat to add additional group links if desired.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
If a user is a member of multiple SAML groups mapped to the same GitLab group,
|
|
||||||
the user gets the highest access level from the groups. For example, if one group
|
|
||||||
is linked as `Guest` and another `Maintainer`, a user in both groups gets `Maintainer`
|
|
||||||
access.
|
|
||||||
|
|
||||||
Users granted:
|
|
||||||
|
|
||||||
- A higher role with Group Sync are displayed as having
|
|
||||||
[direct membership](../../project/members/#display-direct-members) of the group.
|
|
||||||
- A lower or the same role with Group Sync are displayed as having
|
|
||||||
[inherited membership](../../project/members/#display-inherited-members) of the group.
|
|
||||||
|
|
||||||
### Automatic member removal
|
|
||||||
|
|
||||||
After a group sync, for GitLab subgroups, users who are not members of a mapped SAML
|
|
||||||
group are removed from the group.
|
|
||||||
|
|
||||||
FLAG:
|
|
||||||
In [GitLab 15.1 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/364144), on GitLab.com, users in the top-level
|
|
||||||
group are assigned the [default membership role](#role) rather than removed. This setting is enabled with the
|
|
||||||
`saml_group_sync_retain_default_membership` feature flag and can be configured by GitLab.com administrators only.
|
|
||||||
|
|
||||||
For example, in the following diagram:
|
|
||||||
|
|
||||||
- Alex Garcia signs into GitLab and is removed from GitLab Group C because they don't belong
|
|
||||||
to SAML Group C.
|
|
||||||
- Sidney Jones belongs to SAML Group C, but is not added to GitLab Group C because they have
|
|
||||||
not yet signed in.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TB
|
|
||||||
subgraph SAML users
|
|
||||||
SAMLUserA[Sidney Jones]
|
|
||||||
SAMLUserB[Zhang Wei]
|
|
||||||
SAMLUserC[Alex Garcia]
|
|
||||||
SAMLUserD[Charlie Smith]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph SAML groups
|
|
||||||
SAMLGroupA["Group A"] --> SAMLGroupB["Group B"]
|
|
||||||
SAMLGroupA --> SAMLGroupC["Group C"]
|
|
||||||
SAMLGroupA --> SAMLGroupD["Group D"]
|
|
||||||
end
|
|
||||||
|
|
||||||
SAMLGroupB --> |Member|SAMLUserA
|
|
||||||
SAMLGroupB --> |Member|SAMLUserB
|
|
||||||
|
|
||||||
SAMLGroupC --> |Member|SAMLUserA
|
|
||||||
SAMLGroupC --> |Member|SAMLUserB
|
|
||||||
|
|
||||||
SAMLGroupD --> |Member|SAMLUserD
|
|
||||||
SAMLGroupD --> |Member|SAMLUserC
|
|
||||||
```
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TB
|
|
||||||
subgraph GitLab users
|
|
||||||
GitLabUserA[Sidney Jones]
|
|
||||||
GitLabUserB[Zhang Wei]
|
|
||||||
GitLabUserC[Alex Garcia]
|
|
||||||
GitLabUserD[Charlie Smith]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph GitLab groups
|
|
||||||
GitLabGroupA["Group A (SAML configured)"] --> GitLabGroupB["Group B (SAML Group Link not configured)"]
|
|
||||||
GitLabGroupA --> GitLabGroupC["Group C (SAML Group Link configured)"]
|
|
||||||
GitLabGroupA --> GitLabGroupD["Group D (SAML Group Link configured)"]
|
|
||||||
end
|
|
||||||
|
|
||||||
GitLabGroupB --> |Member|GitLabUserA
|
|
||||||
|
|
||||||
GitLabGroupC --> |Member|GitLabUserB
|
|
||||||
GitLabGroupC --> |Member|GitLabUserC
|
|
||||||
|
|
||||||
GitLabGroupD --> |Member|GitLabUserC
|
|
||||||
GitLabGroupD --> |Member|GitLabUserD
|
|
||||||
```
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TB
|
|
||||||
subgraph GitLab users
|
|
||||||
GitLabUserA[Sidney Jones]
|
|
||||||
GitLabUserB[Zhang Wei]
|
|
||||||
GitLabUserC[Alex Garcia]
|
|
||||||
GitLabUserD[Charlie Smith]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph GitLab groups after Alex Garcia signs in
|
|
||||||
GitLabGroupA[Group A]
|
|
||||||
GitLabGroupA["Group A (SAML configured)"] --> GitLabGroupB["Group B (SAML Group Link not configured)"]
|
|
||||||
GitLabGroupA --> GitLabGroupC["Group C (SAML Group Link configured)"]
|
|
||||||
GitLabGroupA --> GitLabGroupD["Group D (SAML Group Link configured)"]
|
|
||||||
end
|
|
||||||
|
|
||||||
GitLabGroupB --> |Member|GitLabUserA
|
|
||||||
GitLabGroupC --> |Member|GitLabUserB
|
|
||||||
GitLabGroupD --> |Member|GitLabUserC
|
|
||||||
GitLabGroupD --> |Member|GitLabUserD
|
|
||||||
```
|
|
||||||
|
|
||||||
## Passwords for users created via SAML SSO for Groups
|
## Passwords for users created via SAML SSO for Groups
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ module Atlassian
|
||||||
responses.compact
|
responses.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Fetch user information for the given account.
|
||||||
|
# https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-users/#api-rest-api-3-user-get
|
||||||
def user_info(account_id)
|
def user_info(account_id)
|
||||||
r = get('/rest/api/3/user', { accountId: account_id, expand: 'groups' })
|
r = get('/rest/api/3/user', { accountId: account_id, expand: 'groups' })
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ module Gitlab
|
||||||
group_testing_hook: { threshold: 5, interval: 1.minute },
|
group_testing_hook: { threshold: 5, interval: 1.minute },
|
||||||
profile_add_new_email: { threshold: 5, interval: 1.minute },
|
profile_add_new_email: { threshold: 5, interval: 1.minute },
|
||||||
web_hook_calls: { interval: 1.minute },
|
web_hook_calls: { interval: 1.minute },
|
||||||
|
web_hook_calls_mid: { interval: 1.minute },
|
||||||
|
web_hook_calls_low: { interval: 1.minute },
|
||||||
users_get_by_id: { threshold: -> { application_settings.users_get_by_id_limit }, interval: 10.minutes },
|
users_get_by_id: { threshold: -> { application_settings.users_get_by_id_limit }, interval: 10.minutes },
|
||||||
username_exists: { threshold: 20, interval: 1.minute },
|
username_exists: { threshold: 20, interval: 1.minute },
|
||||||
user_sign_up: { threshold: 20, interval: 1.minute },
|
user_sign_up: { threshold: 20, interval: 1.minute },
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module WebHooks
|
||||||
|
class RateLimiter
|
||||||
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
|
LIMIT_NAME = :web_hook_calls
|
||||||
|
NO_LIMIT = 0
|
||||||
|
# SystemHooks (instance admin hooks) and ServiceHooks (integration hooks)
|
||||||
|
# are not rate-limited.
|
||||||
|
EXCLUDED_HOOK_TYPES = %w(SystemHook ServiceHook).freeze
|
||||||
|
|
||||||
|
def initialize(hook)
|
||||||
|
@hook = hook
|
||||||
|
@parent = hook.parent
|
||||||
|
end
|
||||||
|
|
||||||
|
# Increments the rate-limit counter.
|
||||||
|
# Returns true if the hook should be rate-limited.
|
||||||
|
def rate_limit!
|
||||||
|
return false if no_limit?
|
||||||
|
|
||||||
|
::Gitlab::ApplicationRateLimiter.throttled?(
|
||||||
|
limit_name,
|
||||||
|
scope: [root_namespace],
|
||||||
|
threshold: limit
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns true if the hook is currently over its rate-limit.
|
||||||
|
# It does not increment the rate-limit counter.
|
||||||
|
def rate_limited?
|
||||||
|
return false if no_limit?
|
||||||
|
|
||||||
|
Gitlab::ApplicationRateLimiter.peek(
|
||||||
|
limit_name,
|
||||||
|
scope: [root_namespace],
|
||||||
|
threshold: limit
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def limit
|
||||||
|
strong_memoize(:limit) do
|
||||||
|
next NO_LIMIT if hook.class.name.in?(EXCLUDED_HOOK_TYPES)
|
||||||
|
|
||||||
|
root_namespace.actual_limits.limit_for(limit_name) || NO_LIMIT
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :hook, :parent
|
||||||
|
|
||||||
|
def no_limit?
|
||||||
|
limit == NO_LIMIT
|
||||||
|
end
|
||||||
|
|
||||||
|
def root_namespace
|
||||||
|
@root_namespace ||= parent.root_ancestor
|
||||||
|
end
|
||||||
|
|
||||||
|
def limit_name
|
||||||
|
LIMIT_NAME
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Gitlab::WebHooks::RateLimiter.prepend_mod
|
||||||
|
|
@ -21742,9 +21742,15 @@ msgstr ""
|
||||||
msgid "Jira-GitLab user mapping template"
|
msgid "Jira-GitLab user mapping template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "JiraConnect|Cannot find namespace. Make sure you have sufficient permissions."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "JiraConnect|Configure your Jira Connect Application ID."
|
msgid "JiraConnect|Configure your Jira Connect Application ID."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "JiraConnect|Could not fetch user information from Jira. Check the permissions in Jira and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "JiraConnect|Create branch for Jira issue %{jiraIssue}"
|
msgid "JiraConnect|Create branch for Jira issue %{jiraIssue}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -21763,6 +21769,9 @@ msgstr ""
|
||||||
msgid "JiraConnect|New branch was successfully created."
|
msgid "JiraConnect|New branch was successfully created."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "JiraConnect|The Jira user is not a site administrator. Check the permissions in Jira and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "JiraConnect|You can now close this window and return to Jira."
|
msgid "JiraConnect|You can now close this window and return to Jira."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ module QA
|
||||||
element :ci_variable_delete_button
|
element :ci_variable_delete_button
|
||||||
end
|
end
|
||||||
|
|
||||||
view 'app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue' do
|
view 'app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_table.vue' do
|
||||||
element :ci_variable_table_content
|
element :ci_variable_table_content
|
||||||
element :add_ci_variable_button
|
element :add_ci_variable_button
|
||||||
element :edit_ci_variable_button
|
element :edit_ci_variable_button
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,18 @@ RSpec.describe 'Group variables', :js do
|
||||||
group.add_owner(user)
|
group.add_owner(user)
|
||||||
gitlab_sign_in(user)
|
gitlab_sign_in(user)
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with disabled ff `ci_variable_settings_graphql' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(ci_variable_settings_graphql: false)
|
||||||
visit page_path
|
visit page_path
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'variable list'
|
it_behaves_like 'variable list'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO: Uncomment when the new graphQL app for variable settings
|
||||||
|
# is enabled.
|
||||||
|
# it_behaves_like 'variable list'
|
||||||
|
end
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,13 @@ RSpec.describe 'Project variables', :js do
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
project.add_maintainer(user)
|
project.add_maintainer(user)
|
||||||
project.variables << variable
|
project.variables << variable
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: Add same tests but with FF enabled context when
|
||||||
|
# the new graphQL app for variable settings is enabled.
|
||||||
|
context 'with disabled ff `ci_variable_settings_graphql' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(ci_variable_settings_graphql: false)
|
||||||
visit page_path
|
visit page_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -37,3 +44,4 @@ RSpec.describe 'Project variables', :js do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,33 @@ RSpec.describe 'Projects > Members > Manage members', :js do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when owner' do
|
||||||
|
it 'uses ProjectMember access_level_roles for the invite members modal access option', :aggregate_failures do
|
||||||
|
visit_members_page
|
||||||
|
|
||||||
|
click_on 'Invite members'
|
||||||
|
|
||||||
|
click_on 'Guest'
|
||||||
|
wait_for_requests
|
||||||
|
|
||||||
|
page.within '.dropdown-menu' do
|
||||||
|
expect(page).to have_button('Guest')
|
||||||
|
expect(page).to have_button('Reporter')
|
||||||
|
expect(page).to have_button('Developer')
|
||||||
|
expect(page).to have_button('Maintainer')
|
||||||
|
expect(page).to have_button('Owner')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when maintainer' do
|
||||||
|
let(:maintainer) { create(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.add_maintainer(maintainer)
|
||||||
|
sign_in(maintainer)
|
||||||
|
end
|
||||||
|
|
||||||
it 'uses ProjectMember access_level_roles for the invite members modal access option', :aggregate_failures do
|
it 'uses ProjectMember access_level_roles for the invite members modal access option', :aggregate_failures do
|
||||||
visit_members_page
|
visit_members_page
|
||||||
|
|
||||||
|
|
@ -64,6 +91,7 @@ RSpec.describe 'Projects > Members > Manage members', :js do
|
||||||
expect(page).not_to have_button('Owner')
|
expect(page).not_to have_button('Owner')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'remove user from project' do
|
it 'remove user from project' do
|
||||||
other_user = create(:user)
|
other_user = create(:user)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
|
||||||
import { mount } from '@vue/test-utils';
|
import { mount } from '@vue/test-utils';
|
||||||
import Vue, { nextTick } from 'vue';
|
import Vue, { nextTick } from 'vue';
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
|
import LegacyCiEnvironmentsDropdown from '~/ci_variable_list/components/legacy_ci_environments_dropdown.vue';
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
|
||||||
|
|
@ -20,7 +20,7 @@ describe('Ci environments dropdown', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
wrapper = mount(CiEnvironmentsDropdown, {
|
wrapper = mount(LegacyCiEnvironmentsDropdown, {
|
||||||
store,
|
store,
|
||||||
propsData: {
|
propsData: {
|
||||||
value: term,
|
value: term,
|
||||||
|
|
@ -4,7 +4,7 @@ import Vue from 'vue';
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
import { mockTracking } from 'helpers/tracking_helper';
|
import { mockTracking } from 'helpers/tracking_helper';
|
||||||
import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
|
import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
|
||||||
import CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue';
|
import LegacyCiVariableModal from '~/ci_variable_list/components/legacy_ci_variable_modal.vue';
|
||||||
import {
|
import {
|
||||||
AWS_ACCESS_KEY_ID,
|
AWS_ACCESS_KEY_ID,
|
||||||
EVENT_LABEL,
|
EVENT_LABEL,
|
||||||
|
|
@ -30,7 +30,7 @@ describe('Ci variable modal', () => {
|
||||||
isGroup: options.isGroup,
|
isGroup: options.isGroup,
|
||||||
environmentScopeLink: '/help/environments',
|
environmentScopeLink: '/help/environments',
|
||||||
});
|
});
|
||||||
wrapper = method(CiVariableModal, {
|
wrapper = method(LegacyCiVariableModal, {
|
||||||
attachTo: document.body,
|
attachTo: document.body,
|
||||||
stubs: {
|
stubs: {
|
||||||
GlModal: ModalStub,
|
GlModal: ModalStub,
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
import CiVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
|
import LegacyCiVariableSettings from '~/ci_variable_list/components/legacy_ci_variable_settings.vue';
|
||||||
import createStore from '~/ci_variable_list/store';
|
import createStore from '~/ci_variable_list/store';
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
|
@ -15,7 +15,7 @@ describe('Ci variable table', () => {
|
||||||
store = createStore();
|
store = createStore();
|
||||||
store.state.isGroup = groupState;
|
store.state.isGroup = groupState;
|
||||||
jest.spyOn(store, 'dispatch').mockImplementation();
|
jest.spyOn(store, 'dispatch').mockImplementation();
|
||||||
wrapper = shallowMount(CiVariableSettings, {
|
wrapper = shallowMount(LegacyCiVariableSettings, {
|
||||||
store,
|
store,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
import CiVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
|
import LegacyCiVariableTable from '~/ci_variable_list/components/legacy_ci_variable_table.vue';
|
||||||
import createStore from '~/ci_variable_list/store';
|
import createStore from '~/ci_variable_list/store';
|
||||||
import mockData from '../services/mock_data';
|
import mockData from '../services/mock_data';
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@ describe('Ci variable table', () => {
|
||||||
const createComponent = () => {
|
const createComponent = () => {
|
||||||
store = createStore();
|
store = createStore();
|
||||||
jest.spyOn(store, 'dispatch').mockImplementation();
|
jest.spyOn(store, 'dispatch').mockImplementation();
|
||||||
wrapper = mountExtended(CiVariableTable, {
|
wrapper = mountExtended(LegacyCiVariableTable, {
|
||||||
attachTo: document.body,
|
attachTo: document.body,
|
||||||
store,
|
store,
|
||||||
});
|
});
|
||||||
|
|
@ -355,30 +355,6 @@ RSpec.describe ProjectsHelper do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#permissible_access_level_roles' do
|
|
||||||
let_it_be(:owner) { create(:user) }
|
|
||||||
let_it_be(:maintainer) { create(:user) }
|
|
||||||
let_it_be(:group) { create(:group) }
|
|
||||||
let_it_be(:project) { create(:project, group: group) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
project.add_owner(owner)
|
|
||||||
project.add_maintainer(maintainer)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when member can manage owners' do
|
|
||||||
it 'returns Gitlab::Access.options_with_owner' do
|
|
||||||
expect(helper.permissible_access_level_roles(owner, project)).to eq(Gitlab::Access.options_with_owner)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when member cannot manage owners' do
|
|
||||||
it 'returns Gitlab::Access.options' do
|
|
||||||
expect(helper.permissible_access_level_roles(maintainer, project)).to eq(Gitlab::Access.options)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'default_clone_protocol' do
|
describe 'default_clone_protocol' do
|
||||||
context 'when user is not logged in and gitlab protocol is HTTP' do
|
context 'when user is not logged in and gitlab protocol is HTTP' do
|
||||||
it 'returns HTTP' do
|
it 'returns HTTP' do
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
||||||
RSpec.describe Atlassian::JiraConnect::Client do
|
RSpec.describe Atlassian::JiraConnect::Client do
|
||||||
include StubRequests
|
include StubRequests
|
||||||
|
|
||||||
subject { described_class.new('https://gitlab-test.atlassian.net', 'sample_secret') }
|
subject(:client) { described_class.new('https://gitlab-test.atlassian.net', 'sample_secret') }
|
||||||
|
|
||||||
let_it_be(:project) { create_default(:project, :repository) }
|
let_it_be(:project) { create_default(:project, :repository) }
|
||||||
let_it_be(:mrs_by_title) { create_list(:merge_request, 4, :unique_branches, :jira_title) }
|
let_it_be(:mrs_by_title) { create_list(:merge_request, 4, :unique_branches, :jira_title) }
|
||||||
|
|
@ -413,4 +413,41 @@ RSpec.describe Atlassian::JiraConnect::Client do
|
||||||
expect { subject.send(:store_dev_info, project: project, merge_requests: merge_requests) }.not_to exceed_query_limit(control_count)
|
expect { subject.send(:store_dev_info, project: project, merge_requests: merge_requests) }.not_to exceed_query_limit(control_count)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#user_info' do
|
||||||
|
let(:account_id) { '12345' }
|
||||||
|
let(:response_body) do
|
||||||
|
{
|
||||||
|
groups: {
|
||||||
|
items: [
|
||||||
|
{ name: 'site-admins' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_full_request("https://gitlab-test.atlassian.net/rest/api/3/user?accountId=#{account_id}&expand=groups")
|
||||||
|
.to_return(status: response_status, body: response_body, headers: { 'Content-Type': 'application/json' })
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a successful response' do
|
||||||
|
let(:response_status) { 200 }
|
||||||
|
|
||||||
|
it 'returns a JiraUser instance' do
|
||||||
|
jira_user = client.user_info(account_id)
|
||||||
|
|
||||||
|
expect(jira_user).to be_a(Atlassian::JiraConnect::JiraUser)
|
||||||
|
expect(jira_user).to be_site_admin
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a failed response' do
|
||||||
|
let(:response_status) { 401 }
|
||||||
|
|
||||||
|
it 'returns nil' do
|
||||||
|
expect(client.user_info(account_id)).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ RSpec.describe Gitlab::DatabaseImporters::InstanceAdministrators::CreateGroup do
|
||||||
create(:user)
|
create(:user)
|
||||||
|
|
||||||
expect(result[:status]).to eq(:success)
|
expect(result[:status]).to eq(:success)
|
||||||
|
group.reset
|
||||||
expect(group.members.collect(&:user)).to contain_exactly(user, admin1, admin2)
|
expect(group.members.collect(&:user)).to contain_exactly(user, admin1, admin2)
|
||||||
expect(group.members.collect(&:access_level)).to contain_exactly(
|
expect(group.members.collect(&:access_level)).to contain_exactly(
|
||||||
Gitlab::Access::OWNER,
|
Gitlab::Access::OWNER,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Gitlab::WebHooks::RateLimiter, :clean_gitlab_redis_rate_limiting do
|
||||||
|
let_it_be(:plan) { create(:default_plan) }
|
||||||
|
let_it_be_with_reload(:project_hook) { create(:project_hook) }
|
||||||
|
let_it_be_with_reload(:system_hook) { create(:system_hook) }
|
||||||
|
let_it_be_with_reload(:integration_hook) { create(:jenkins_integration).service_hook }
|
||||||
|
let_it_be(:limit) { 1 }
|
||||||
|
|
||||||
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
describe '#rate_limit!' do
|
||||||
|
def rate_limit!(hook)
|
||||||
|
described_class.new(hook).rate_limit!
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'a hook that is never rate limited' do
|
||||||
|
specify do
|
||||||
|
expect(Gitlab::ApplicationRateLimiter).not_to receive(:throttled?)
|
||||||
|
|
||||||
|
expect(rate_limit!(hook)).to eq(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there is no plan limit' do
|
||||||
|
where(:hook) { [ref(:project_hook), ref(:system_hook), ref(:integration_hook)] }
|
||||||
|
|
||||||
|
with_them { it_behaves_like 'a hook that is never rate limited' }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there is a plan limit' do
|
||||||
|
before_all do
|
||||||
|
create(:plan_limits, plan: plan, web_hook_calls: limit)
|
||||||
|
end
|
||||||
|
|
||||||
|
where(:hook, :limitless_hook_type) do
|
||||||
|
ref(:project_hook) | false
|
||||||
|
ref(:system_hook) | true
|
||||||
|
ref(:integration_hook) | true
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
if params[:limitless_hook_type]
|
||||||
|
it_behaves_like 'a hook that is never rate limited'
|
||||||
|
else
|
||||||
|
it 'rate limits the hook, returning true when rate limited' do
|
||||||
|
expect(Gitlab::ApplicationRateLimiter).to receive(:throttled?)
|
||||||
|
.exactly(3).times
|
||||||
|
.and_call_original
|
||||||
|
|
||||||
|
freeze_time do
|
||||||
|
limit.times { expect(rate_limit!(hook)).to eq(false) }
|
||||||
|
expect(rate_limit!(hook)).to eq(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
travel_to(1.day.from_now) do
|
||||||
|
expect(rate_limit!(hook)).to eq(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'rate limit scope' do
|
||||||
|
it 'rate limits all hooks from the same namespace', :freeze_time do
|
||||||
|
create(:plan_limits, plan: plan, web_hook_calls: limit)
|
||||||
|
project_hook_in_different_namespace = create(:project_hook)
|
||||||
|
project_hook_in_same_namespace = create(:project_hook,
|
||||||
|
project: create(:project, namespace: project_hook.project.namespace)
|
||||||
|
)
|
||||||
|
|
||||||
|
limit.times { expect(rate_limit!(project_hook)).to eq(false) }
|
||||||
|
expect(rate_limit!(project_hook)).to eq(true)
|
||||||
|
expect(rate_limit!(project_hook_in_same_namespace)).to eq(true)
|
||||||
|
expect(rate_limit!(project_hook_in_different_namespace)).to eq(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#rate_limited?' do
|
||||||
|
subject { described_class.new(hook).rate_limited? }
|
||||||
|
|
||||||
|
context 'when no plan limit has been defined' do
|
||||||
|
where(:hook) { [ref(:project_hook), ref(:system_hook), ref(:integration_hook)] }
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it { is_expected.to eq(false) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there is a plan limit' do
|
||||||
|
before_all do
|
||||||
|
create(:plan_limits, plan: plan, web_hook_calls: limit)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when hook is not rate-limited' do
|
||||||
|
where(:hook) { [ref(:project_hook), ref(:system_hook), ref(:integration_hook)] }
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it { is_expected.to eq(false) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when hook is rate-limited' do
|
||||||
|
before do
|
||||||
|
allow(Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
where(:hook, :limitless_hook_type) do
|
||||||
|
ref(:project_hook) | false
|
||||||
|
ref(:system_hook) | true
|
||||||
|
ref(:integration_hook) | true
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it { is_expected.to eq(!limitless_hook_type) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
require_migration!
|
||||||
|
|
||||||
|
RSpec.describe AddWebHookCallsToPlanLimitsPaidTiers do
|
||||||
|
let_it_be(:plans) { table(:plans) }
|
||||||
|
let_it_be(:plan_limits) { table(:plan_limits) }
|
||||||
|
|
||||||
|
context 'when on Gitlab.com' do
|
||||||
|
let(:free_plan) { plans.create!(name: 'free') }
|
||||||
|
let(:bronze_plan) { plans.create!(name: 'bronze') }
|
||||||
|
let(:silver_plan) { plans.create!(name: 'silver') }
|
||||||
|
let(:gold_plan) { plans.create!(name: 'gold') }
|
||||||
|
let(:premium_plan) { plans.create!(name: 'premium') }
|
||||||
|
let(:premium_trial_plan) { plans.create!(name: 'premium_trial') }
|
||||||
|
let(:ultimate_plan) { plans.create!(name: 'ultimate') }
|
||||||
|
let(:ultimate_trial_plan) { plans.create!(name: 'ultimate_trial') }
|
||||||
|
let(:opensource_plan) { plans.create!(name: 'opensource') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Gitlab).to receive(:com?).and_return(true)
|
||||||
|
# 120 is the value for 'free' migrated in `db/migrate/20210601131742_update_web_hook_calls_limit.rb`
|
||||||
|
plan_limits.create!(plan_id: free_plan.id, web_hook_calls: 120)
|
||||||
|
plan_limits.create!(plan_id: bronze_plan.id)
|
||||||
|
plan_limits.create!(plan_id: silver_plan.id)
|
||||||
|
plan_limits.create!(plan_id: gold_plan.id)
|
||||||
|
plan_limits.create!(plan_id: premium_plan.id)
|
||||||
|
plan_limits.create!(plan_id: premium_trial_plan.id)
|
||||||
|
plan_limits.create!(plan_id: ultimate_plan.id)
|
||||||
|
plan_limits.create!(plan_id: ultimate_trial_plan.id)
|
||||||
|
plan_limits.create!(plan_id: opensource_plan.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'correctly migrates up and down' do
|
||||||
|
reversible_migration do |migration|
|
||||||
|
migration.before -> {
|
||||||
|
expect(
|
||||||
|
plan_limits.pluck(:plan_id, :web_hook_calls, :web_hook_calls_mid, :web_hook_calls_low)
|
||||||
|
).to contain_exactly(
|
||||||
|
[free_plan.id, 120, 0, 0],
|
||||||
|
[bronze_plan.id, 0, 0, 0],
|
||||||
|
[silver_plan.id, 0, 0, 0],
|
||||||
|
[gold_plan.id, 0, 0, 0],
|
||||||
|
[premium_plan.id, 0, 0, 0],
|
||||||
|
[premium_trial_plan.id, 0, 0, 0],
|
||||||
|
[ultimate_plan.id, 0, 0, 0],
|
||||||
|
[ultimate_trial_plan.id, 0, 0, 0],
|
||||||
|
[opensource_plan.id, 0, 0, 0]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
migration.after -> {
|
||||||
|
expect(
|
||||||
|
plan_limits.pluck(:plan_id, :web_hook_calls, :web_hook_calls_mid, :web_hook_calls_low)
|
||||||
|
).to contain_exactly(
|
||||||
|
[free_plan.id, 500, 500, 500],
|
||||||
|
[bronze_plan.id, 4_000, 2_800, 1_600],
|
||||||
|
[silver_plan.id, 4_000, 2_800, 1_600],
|
||||||
|
[gold_plan.id, 13_000, 9_000, 6_000],
|
||||||
|
[premium_plan.id, 4_000, 2_800, 1_600],
|
||||||
|
[premium_trial_plan.id, 4_000, 2_800, 1_600],
|
||||||
|
[ultimate_plan.id, 13_000, 9_000, 6_000],
|
||||||
|
[ultimate_trial_plan.id, 13_000, 9_000, 6_000],
|
||||||
|
[opensource_plan.id, 13_000, 9_000, 6_000]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when on self hosted' do
|
||||||
|
let(:default_plan) { plans.create!(name: 'default') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Gitlab).to receive(:com?).and_return(false)
|
||||||
|
|
||||||
|
plan_limits.create!(plan_id: default_plan.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does nothing' do
|
||||||
|
reversible_migration do |migration|
|
||||||
|
migration.before -> {
|
||||||
|
expect(
|
||||||
|
plan_limits.pluck(:plan_id, :web_hook_calls, :web_hook_calls_mid, :web_hook_calls_low)
|
||||||
|
).to contain_exactly(
|
||||||
|
[default_plan.id, 0, 0, 0]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
migration.after -> {
|
||||||
|
expect(
|
||||||
|
plan_limits.pluck(:plan_id, :web_hook_calls, :web_hook_calls_mid, :web_hook_calls_low)
|
||||||
|
).to contain_exactly(
|
||||||
|
[default_plan.id, 0, 0, 0]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -40,6 +40,39 @@ RSpec.describe Clusters::Agent do
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(matching_name) }
|
it { is_expected.to contain_exactly(matching_name) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.has_vulnerabilities' do
|
||||||
|
let_it_be(:without_vulnerabilities) { create(:cluster_agent, has_vulnerabilities: false) }
|
||||||
|
let_it_be(:with_vulnerabilities) { create(:cluster_agent, has_vulnerabilities: true) }
|
||||||
|
|
||||||
|
context 'when value is not provided' do
|
||||||
|
subject { described_class.has_vulnerabilities }
|
||||||
|
|
||||||
|
it 'returns agents which have vulnerabilities' do
|
||||||
|
is_expected.to contain_exactly(with_vulnerabilities)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when value is provided' do
|
||||||
|
subject { described_class.has_vulnerabilities(value) }
|
||||||
|
|
||||||
|
context 'as true' do
|
||||||
|
let(:value) { true }
|
||||||
|
|
||||||
|
it 'returns agents which have vulnerabilities' do
|
||||||
|
is_expected.to contain_exactly(with_vulnerabilities)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'as false' do
|
||||||
|
let(:value) { false }
|
||||||
|
|
||||||
|
it 'returns agents which do not have vulnerabilities' do
|
||||||
|
is_expected.to contain_exactly(without_vulnerabilities)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'validation' do
|
describe 'validation' do
|
||||||
|
|
|
||||||
|
|
@ -31,15 +31,6 @@ RSpec.describe ProjectHook do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#rate_limit' do
|
|
||||||
let_it_be(:plan_limits) { create(:plan_limits, :default_plan, web_hook_calls: 100) }
|
|
||||||
let_it_be(:hook) { create(:project_hook) }
|
|
||||||
|
|
||||||
it 'returns the default limit' do
|
|
||||||
expect(hook.rate_limit).to be(100)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#parent' do
|
describe '#parent' do
|
||||||
it 'returns the associated project' do
|
it 'returns the associated project' do
|
||||||
project = build(:project)
|
project = build(:project)
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,6 @@ RSpec.describe ServiceHook do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#rate_limit' do
|
|
||||||
let(:hook) { build(:service_hook) }
|
|
||||||
|
|
||||||
it 'returns nil' do
|
|
||||||
expect(hook.rate_limit).to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#parent' do
|
describe '#parent' do
|
||||||
let(:hook) { build(:service_hook, integration: integration) }
|
let(:hook) { build(:service_hook, integration: integration) }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -185,14 +185,6 @@ RSpec.describe SystemHook do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#rate_limit' do
|
|
||||||
let(:hook) { build(:system_hook) }
|
|
||||||
|
|
||||||
it 'returns nil' do
|
|
||||||
expect(hook.rate_limit).to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#application_context' do
|
describe '#application_context' do
|
||||||
let(:hook) { build(:system_hook) }
|
let(:hook) { build(:system_hook) }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -493,31 +493,30 @@ RSpec.describe WebHook do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#rate_limited?' do
|
describe '#rate_limited?' do
|
||||||
context 'when there are rate limits' do
|
it 'is false when hook has not been rate limited' do
|
||||||
before do
|
expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
|
||||||
allow(hook).to receive(:rate_limit).and_return(3)
|
expect(rate_limiter).to receive(:rate_limited?).and_return(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is false when hook has not been rate limited' do
|
|
||||||
expect(Gitlab::ApplicationRateLimiter).to receive(:peek).and_return(false)
|
|
||||||
expect(hook).not_to be_rate_limited
|
expect(hook).not_to be_rate_limited
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is true when hook has been rate limited' do
|
it 'is true when hook has been rate limited' do
|
||||||
expect(Gitlab::ApplicationRateLimiter).to receive(:peek).and_return(true)
|
expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
|
||||||
|
expect(rate_limiter).to receive(:rate_limited?).and_return(true)
|
||||||
|
end
|
||||||
|
|
||||||
expect(hook).to be_rate_limited
|
expect(hook).to be_rate_limited
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when there are no rate limits' do
|
describe '#rate_limit' do
|
||||||
before do
|
it 'returns the hook rate limit' do
|
||||||
allow(hook).to receive(:rate_limit).and_return(nil)
|
expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
|
||||||
|
expect(rate_limiter).to receive(:limit).and_return(10)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not call Gitlab::ApplicationRateLimiter, and is false' do
|
expect(hook.rate_limit).to eq(10)
|
||||||
expect(Gitlab::ApplicationRateLimiter).not_to receive(:peek)
|
|
||||||
expect(hook).not_to be_rate_limited
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,16 @@ RSpec.describe GroupMember do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#permissible_access_level_roles' do
|
||||||
|
let_it_be(:group) { create(:group) }
|
||||||
|
|
||||||
|
it 'returns Gitlab::Access.options_with_owner' do
|
||||||
|
result = described_class.permissible_access_level_roles(group.first_owner, group)
|
||||||
|
|
||||||
|
expect(result).to eq(Gitlab::Access.options_with_owner)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it_behaves_like 'members notifications', :group
|
it_behaves_like 'members notifications', :group
|
||||||
|
|
||||||
describe '#namespace_id' do
|
describe '#namespace_id' do
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,30 @@ RSpec.describe ProjectMember do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#permissible_access_level_roles' do
|
||||||
|
let_it_be(:owner) { create(:user) }
|
||||||
|
let_it_be(:maintainer) { create(:user) }
|
||||||
|
let_it_be(:group) { create(:group) }
|
||||||
|
let_it_be(:project) { create(:project, group: group) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.add_owner(owner)
|
||||||
|
project.add_maintainer(maintainer)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when member can manage owners' do
|
||||||
|
it 'returns Gitlab::Access.options_with_owner' do
|
||||||
|
expect(described_class.permissible_access_level_roles(owner, project)).to eq(Gitlab::Access.options_with_owner)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when member cannot manage owners' do
|
||||||
|
it 'returns Gitlab::Access.options' do
|
||||||
|
expect(described_class.permissible_access_level_roles(maintainer, project)).to eq(Gitlab::Access.options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#real_source_type' do
|
describe '#real_source_type' do
|
||||||
subject { create(:project_member).real_source_type }
|
subject { create(:project_member).real_source_type }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -213,6 +213,8 @@ RSpec.describe PlanLimits do
|
||||||
storage_size_limit
|
storage_size_limit
|
||||||
daily_invites
|
daily_invites
|
||||||
web_hook_calls
|
web_hook_calls
|
||||||
|
web_hook_calls_mid
|
||||||
|
web_hook_calls_low
|
||||||
ci_daily_pipeline_schedule_triggers
|
ci_daily_pipeline_schedule_triggers
|
||||||
repository_size
|
repository_size
|
||||||
security_policy_scan_execution_schedules
|
security_policy_scan_execution_schedules
|
||||||
|
|
|
||||||
|
|
@ -59,13 +59,13 @@ RSpec.describe API::Invitations do
|
||||||
|
|
||||||
context 'when authenticated as a maintainer/owner' do
|
context 'when authenticated as a maintainer/owner' do
|
||||||
context 'and new member is already a requester' do
|
context 'and new member is already a requester' do
|
||||||
it 'does not transform the requester into a proper member' do
|
it 'transforms the requester into a proper member' do
|
||||||
expect do
|
expect do
|
||||||
post invitations_url(source, maintainer),
|
post invitations_url(source, maintainer),
|
||||||
params: { email: access_requester.email, access_level: Member::MAINTAINER }
|
params: { email: access_requester.email, access_level: Member::MAINTAINER }
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:created)
|
expect(response).to have_gitlab_http_status(:created)
|
||||||
end.not_to change { source.members.count }
|
end.to change { source.members.count }.by(1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -258,12 +258,13 @@ RSpec.describe API::Invitations do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns a message if member already exists" do
|
it "updates an already existing active member" do
|
||||||
post invitations_url(source, maintainer),
|
post invitations_url(source, maintainer),
|
||||||
params: { email: developer.email, access_level: Member::MAINTAINER }
|
params: { email: developer.email, access_level: Member::MAINTAINER }
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:created)
|
expect(response).to have_gitlab_http_status(:created)
|
||||||
expect(json_response['message'][developer.email]).to eq("User already exists in source")
|
expect(json_response['status']).to eq("success")
|
||||||
|
expect(source.members.find_by(user: developer).access_level).to eq Member::MAINTAINER
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns 400 when the invite params of email and user_id are not sent' do
|
it 'returns 400 when the invite params of email and user_id are not sent' do
|
||||||
|
|
@ -328,7 +329,7 @@ RSpec.describe API::Invitations do
|
||||||
|
|
||||||
emails = 'email3@example.com,email4@example.com,email5@example.com,email6@example.com,email7@example.com'
|
emails = 'email3@example.com,email4@example.com,email5@example.com,email6@example.com,email7@example.com'
|
||||||
|
|
||||||
unresolved_n_plus_ones = 44 # old 48 with 12 per new email, currently there are 11 queries added per email
|
unresolved_n_plus_ones = 40 # currently there are 10 queries added per email
|
||||||
|
|
||||||
expect do
|
expect do
|
||||||
post invitations_url(project, maintainer), params: { email: emails, access_level: Member::DEVELOPER }
|
post invitations_url(project, maintainer), params: { email: emails, access_level: Member::DEVELOPER }
|
||||||
|
|
@ -351,7 +352,7 @@ RSpec.describe API::Invitations do
|
||||||
|
|
||||||
emails = 'email3@example.com,email4@example.com,email5@example.com,email6@example.com,email7@example.com'
|
emails = 'email3@example.com,email4@example.com,email5@example.com,email6@example.com,email7@example.com'
|
||||||
|
|
||||||
unresolved_n_plus_ones = 67 # currently there are 11 queries added per email
|
unresolved_n_plus_ones = 59 # currently there are 10 queries added per email
|
||||||
|
|
||||||
expect do
|
expect do
|
||||||
post invitations_url(project, maintainer), params: { email: emails, access_level: Member::DEVELOPER }
|
post invitations_url(project, maintainer), params: { email: emails, access_level: Member::DEVELOPER }
|
||||||
|
|
@ -373,7 +374,7 @@ RSpec.describe API::Invitations do
|
||||||
|
|
||||||
emails = 'email3@example.com,email4@example.com,email5@example.com,email6@example.com,email7@example.com'
|
emails = 'email3@example.com,email4@example.com,email5@example.com,email6@example.com,email7@example.com'
|
||||||
|
|
||||||
unresolved_n_plus_ones = 36 # old 40 with 10 per new email, currently there are 9 queries added per email
|
unresolved_n_plus_ones = 32 # currently there are 8 queries added per email
|
||||||
|
|
||||||
expect do
|
expect do
|
||||||
post invitations_url(group, maintainer), params: { email: emails, access_level: Member::DEVELOPER }
|
post invitations_url(group, maintainer), params: { email: emails, access_level: Member::DEVELOPER }
|
||||||
|
|
@ -396,7 +397,7 @@ RSpec.describe API::Invitations do
|
||||||
|
|
||||||
emails = 'email3@example.com,email4@example.com,email5@example.com,email6@example.com,email7@example.com'
|
emails = 'email3@example.com,email4@example.com,email5@example.com,email6@example.com,email7@example.com'
|
||||||
|
|
||||||
unresolved_n_plus_ones = 62 # currently there are 9 queries added per email
|
unresolved_n_plus_ones = 56 # currently there are 8 queries added per email
|
||||||
|
|
||||||
expect do
|
expect do
|
||||||
post invitations_url(group, maintainer), params: { email: emails, access_level: Member::DEVELOPER }
|
post invitations_url(group, maintainer), params: { email: emails, access_level: Member::DEVELOPER }
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe JiraConnectSubscriptions::CreateService do
|
RSpec.describe JiraConnectSubscriptions::CreateService do
|
||||||
let(:installation) { create(:jira_connect_installation) }
|
let_it_be(:installation) { create(:jira_connect_installation) }
|
||||||
let(:current_user) { create(:user) }
|
let_it_be(:current_user) { create(:user) }
|
||||||
let(:group) { create(:group) }
|
let_it_be(:group) { create(:group) }
|
||||||
|
|
||||||
let(:path) { group.full_path }
|
let(:path) { group.full_path }
|
||||||
let(:params) { { namespace_path: path, jira_user: jira_user } }
|
let(:params) { { namespace_path: path, jira_user: jira_user } }
|
||||||
let(:jira_user) { double(:JiraUser, site_admin?: true) }
|
let(:jira_user) { double(:JiraUser, site_admin?: true) }
|
||||||
|
|
@ -16,38 +17,31 @@ RSpec.describe JiraConnectSubscriptions::CreateService do
|
||||||
group.add_maintainer(current_user)
|
group.add_maintainer(current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
shared_examples 'a failed execution' do
|
shared_examples 'a failed execution' do |**status_attributes|
|
||||||
it 'does not create a subscription' do
|
it 'does not create a subscription' do
|
||||||
expect { subject }.not_to change { installation.subscriptions.count }
|
expect { subject }.not_to change { installation.subscriptions.count }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns an error status' do
|
it 'returns an error status' do
|
||||||
expect(subject[:status]).to eq(:error)
|
expect(subject[:status]).to eq(:error)
|
||||||
|
expect(subject).to include(status_attributes)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'remote user does not have access' do
|
context 'remote user does not have access' do
|
||||||
let(:jira_user) { double(site_admin?: false) }
|
let(:jira_user) { double(site_admin?: false) }
|
||||||
|
|
||||||
it 'does not create a subscription' do
|
it_behaves_like 'a failed execution',
|
||||||
expect { subject }.not_to change { installation.subscriptions.count }
|
http_status: 403,
|
||||||
end
|
message: 'The Jira user is not a site administrator. Check the permissions in Jira and try again.'
|
||||||
|
|
||||||
it 'returns error' do
|
|
||||||
expect(subject[:status]).to eq(:error)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'remote user cannot be retrieved' do
|
context 'remote user cannot be retrieved' do
|
||||||
let(:jira_user) { nil }
|
let(:jira_user) { nil }
|
||||||
|
|
||||||
it 'does not create a subscription' do
|
it_behaves_like 'a failed execution',
|
||||||
expect { subject }.not_to change { installation.subscriptions.count }
|
http_status: 403,
|
||||||
end
|
message: 'Could not fetch user information from Jira. Check the permissions in Jira and try again.'
|
||||||
|
|
||||||
it 'returns error' do
|
|
||||||
expect(subject[:status]).to eq(:error)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when user does have access' do
|
context 'when user does have access' do
|
||||||
|
|
@ -60,8 +54,8 @@ RSpec.describe JiraConnectSubscriptions::CreateService do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'namespace has projects' do
|
context 'namespace has projects' do
|
||||||
let!(:project_1) { create(:project, group: group) }
|
let_it_be(:project_1) { create(:project, group: group) }
|
||||||
let!(:project_2) { create(:project, group: group) }
|
let_it_be(:project_2) { create(:project, group: group) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_const("#{described_class}::MERGE_REQUEST_SYNC_BATCH_SIZE", 1)
|
stub_const("#{described_class}::MERGE_REQUEST_SYNC_BATCH_SIZE", 1)
|
||||||
|
|
@ -81,12 +75,18 @@ RSpec.describe JiraConnectSubscriptions::CreateService do
|
||||||
context 'when path is invalid' do
|
context 'when path is invalid' do
|
||||||
let(:path) { 'some_invalid_namespace_path' }
|
let(:path) { 'some_invalid_namespace_path' }
|
||||||
|
|
||||||
it_behaves_like 'a failed execution'
|
it_behaves_like 'a failed execution',
|
||||||
|
http_status: 401,
|
||||||
|
message: 'Cannot find namespace. Make sure you have sufficient permissions.'
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when user does not have access' do
|
context 'when user does not have access' do
|
||||||
subject { described_class.new(installation, create(:user), namespace_path: path).execute }
|
let_it_be(:other_group) { create(:group) }
|
||||||
|
|
||||||
it_behaves_like 'a failed execution'
|
let(:path) { other_group.full_path }
|
||||||
|
|
||||||
|
it_behaves_like 'a failed execution',
|
||||||
|
http_status: 401,
|
||||||
|
message: 'Cannot find namespace. Make sure you have sufficient permissions.'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ RSpec.describe Members::CreatorService do
|
||||||
describe '#execute' do
|
describe '#execute' do
|
||||||
it 'raises error for new member on authorization check implementation' do
|
it 'raises error for new member on authorization check implementation' do
|
||||||
expect do
|
expect do
|
||||||
described_class.new(source, user, :maintainer, current_user: current_user).execute
|
described_class.add_user(source, user, :maintainer, current_user: current_user)
|
||||||
end.to raise_error(NotImplementedError)
|
end.to raise_error(NotImplementedError)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -19,7 +19,7 @@ RSpec.describe Members::CreatorService do
|
||||||
source.add_developer(user)
|
source.add_developer(user)
|
||||||
|
|
||||||
expect do
|
expect do
|
||||||
described_class.new(source, user, :maintainer, current_user: current_user).execute
|
described_class.add_user(source, user, :maintainer, current_user: current_user)
|
||||||
end.to raise_error(NotImplementedError)
|
end.to raise_error(NotImplementedError)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'spec_helper'
|
|
||||||
|
|
||||||
RSpec.describe Members::Groups::BulkCreatorService do
|
|
||||||
let_it_be(:source, reload: true) { create(:group, :public) }
|
|
||||||
let_it_be(:current_user) { create(:user) }
|
|
||||||
|
|
||||||
it_behaves_like 'bulk member creation' do
|
|
||||||
let_it_be(:member_type) { GroupMember }
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'owner management'
|
|
||||||
end
|
|
||||||
|
|
@ -3,16 +3,24 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Members::Groups::CreatorService do
|
RSpec.describe Members::Groups::CreatorService do
|
||||||
|
let_it_be(:source, reload: true) { create(:group, :public) }
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
describe '.access_levels' do
|
describe '.access_levels' do
|
||||||
it 'returns Gitlab::Access.options_with_owner' do
|
it 'returns Gitlab::Access.options_with_owner' do
|
||||||
expect(described_class.access_levels).to eq(Gitlab::Access.sym_options_with_owner)
|
expect(described_class.access_levels).to eq(Gitlab::Access.sym_options_with_owner)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#execute' do
|
it_behaves_like 'owner management'
|
||||||
let_it_be(:source, reload: true) { create(:group, :public) }
|
|
||||||
let_it_be(:user) { create(:user) }
|
|
||||||
|
|
||||||
|
describe '.add_users' do
|
||||||
|
it_behaves_like 'bulk member creation' do
|
||||||
|
let_it_be(:member_type) { GroupMember }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.add_user' do
|
||||||
it_behaves_like 'member creation' do
|
it_behaves_like 'member creation' do
|
||||||
let_it_be(:member_type) { GroupMember }
|
let_it_be(:member_type) { GroupMember }
|
||||||
end
|
end
|
||||||
|
|
@ -22,7 +30,7 @@ RSpec.describe Members::Groups::CreatorService do
|
||||||
expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait).once
|
expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait).once
|
||||||
|
|
||||||
1.upto(3) do
|
1.upto(3) do
|
||||||
described_class.new(source, user, :maintainer).execute
|
described_class.add_user(source, user, :maintainer)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -367,20 +367,21 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_
|
||||||
|
|
||||||
context 'when email is already a member with a user on the project' do
|
context 'when email is already a member with a user on the project' do
|
||||||
let!(:existing_member) { create(:project_member, :guest, project: project) }
|
let!(:existing_member) { create(:project_member, :guest, project: project) }
|
||||||
let(:params) { { email: "#{existing_member.user.email}" } }
|
let(:params) { { email: "#{existing_member.user.email}", access_level: ProjectMember::MAINTAINER } }
|
||||||
|
|
||||||
it 'returns an error for the already invited email' do
|
it 'allows re-invite of an already invited email and updates the access_level' do
|
||||||
expect_not_to_create_members
|
expect { result }.not_to change(ProjectMember, :count)
|
||||||
expect(result[:message][existing_member.user.email]).to eq("User already exists in source")
|
expect(result[:status]).to eq(:success)
|
||||||
|
expect(existing_member.reset.access_level).to eq ProjectMember::MAINTAINER
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when email belongs to an existing user as a secondary email' do
|
context 'when email belongs to an existing user as a secondary email' do
|
||||||
let(:secondary_email) { create(:email, email: 'secondary@example.com', user: existing_member.user) }
|
let(:secondary_email) { create(:email, email: 'secondary@example.com', user: existing_member.user) }
|
||||||
let(:params) { { email: "#{secondary_email.email}" } }
|
let(:params) { { email: "#{secondary_email.email}" } }
|
||||||
|
|
||||||
it 'returns an error for the already invited email' do
|
it 'allows re-invite to an already invited email' do
|
||||||
expect_not_to_create_members
|
expect_to_create_members(count: 0)
|
||||||
expect(result[:message][secondary_email.email]).to eq("User already exists in source")
|
expect(result[:status]).to eq(:success)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'spec_helper'
|
|
||||||
|
|
||||||
RSpec.describe Members::Projects::BulkCreatorService do
|
|
||||||
let_it_be(:source, reload: true) { create(:project, :public) }
|
|
||||||
let_it_be(:current_user) { create(:user) }
|
|
||||||
|
|
||||||
it_behaves_like 'bulk member creation' do
|
|
||||||
let_it_be(:member_type) { ProjectMember }
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'owner management'
|
|
||||||
end
|
|
||||||
|
|
@ -3,16 +3,24 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Members::Projects::CreatorService do
|
RSpec.describe Members::Projects::CreatorService do
|
||||||
|
let_it_be(:source, reload: true) { create(:project, :public) }
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
describe '.access_levels' do
|
describe '.access_levels' do
|
||||||
it 'returns Gitlab::Access.sym_options_with_owner' do
|
it 'returns Gitlab::Access.sym_options_with_owner' do
|
||||||
expect(described_class.access_levels).to eq(Gitlab::Access.sym_options_with_owner)
|
expect(described_class.access_levels).to eq(Gitlab::Access.sym_options_with_owner)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#execute' do
|
it_behaves_like 'owner management'
|
||||||
let_it_be(:source, reload: true) { create(:project, :public) }
|
|
||||||
let_it_be(:user) { create(:user) }
|
|
||||||
|
|
||||||
|
describe '.add_users' do
|
||||||
|
it_behaves_like 'bulk member creation' do
|
||||||
|
let_it_be(:member_type) { ProjectMember }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.add_user' do
|
||||||
it_behaves_like 'member creation' do
|
it_behaves_like 'member creation' do
|
||||||
let_it_be(:member_type) { ProjectMember }
|
let_it_be(:member_type) { ProjectMember }
|
||||||
end
|
end
|
||||||
|
|
@ -22,7 +30,7 @@ RSpec.describe Members::Projects::CreatorService do
|
||||||
expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to receive(:bulk_perform_in).once
|
expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to receive(:bulk_perform_in).once
|
||||||
|
|
||||||
1.upto(3) do
|
1.upto(3) do
|
||||||
described_class.new(source, user, :maintainer).execute
|
described_class.add_user(source, user, :maintainer)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue