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
|
||||
# TODO: Uncomment these rules when then `schema` is available
|
||||
#'@graphql-eslint/fragments-on-composite-type': error
|
||||
#'@graphql-eslint/known-argument-names': error
|
||||
#'@graphql-eslint/known-type-names': error
|
||||
'@graphql-eslint/known-argument-names': error
|
||||
'@graphql-eslint/known-type-names': error
|
||||
'@graphql-eslint/no-anonymous-operations': error
|
||||
'@graphql-eslint/unique-operation-name': error
|
||||
'@graphql-eslint/require-id-when-available': error
|
||||
|
|
|
@ -589,7 +589,6 @@ Layout/LineLength:
|
|||
- 'app/services/compare_service.rb'
|
||||
- 'app/services/concerns/base_service_utility.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/rate_limited_service.rb'
|
||||
- 'app/services/concerns/schedule_bulk_repository_shard_moves_methods.rb'
|
||||
|
|
|
@ -1,32 +1,9 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
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']),
|
||||
},
|
||||
};
|
||||
export default {};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<ci-variable-table />
|
||||
<ci-variable-modal />
|
||||
</div>
|
||||
<div class="col-lg-12"></div>
|
||||
</div>
|
||||
</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 VueApollo from 'vue-apollo';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import CiVariableSettings from './components/ci_variable_settings.vue';
|
||||
import LegacyCiVariableSettings from './components/legacy_ci_variable_settings.vue';
|
||||
import createStore from './store';
|
||||
|
||||
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 {
|
||||
endpoint,
|
||||
projectId,
|
||||
|
@ -42,7 +95,7 @@ const mountCiVariableListApp = (containerEl) => {
|
|||
el: containerEl,
|
||||
store,
|
||||
render(createElement) {
|
||||
return createElement(CiVariableSettings);
|
||||
return createElement(LegacyCiVariableSettings);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -50,5 +103,11 @@ const mountCiVariableListApp = (containerEl) => {
|
|||
export default (containerId = 'js-ci-project-variables') => {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -8,6 +8,16 @@ type Item {
|
|||
selectedLabel: String
|
||||
}
|
||||
|
||||
input ItemInput {
|
||||
id: ID!
|
||||
label: String!
|
||||
icon: String
|
||||
selected: Boolean
|
||||
group: Int!
|
||||
category: String
|
||||
selectedLabel: String
|
||||
}
|
||||
|
||||
type Items {
|
||||
nodes: [Item]!
|
||||
}
|
||||
|
@ -17,7 +27,7 @@ extend type Query {
|
|||
}
|
||||
|
||||
extend type Mutation {
|
||||
updateToolbarItem(id: ID!, propsToUpdate: Item!): LocalErrors
|
||||
updateToolbarItem(id: ID!, propsToUpdate: ItemInput!): 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
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
mutation action($action: LocalAction) {
|
||||
mutation action($action: LocalActionInput) {
|
||||
action(action: $action) @client {
|
||||
errors
|
||||
}
|
||||
|
|
|
@ -9,6 +9,11 @@ type LocalEnvironment {
|
|||
autoStopPath: String
|
||||
}
|
||||
|
||||
input LocalActionInput {
|
||||
name: String!
|
||||
playPath: String
|
||||
}
|
||||
|
||||
input LocalEnvironmentInput {
|
||||
id: Int!
|
||||
globalId: ID!
|
||||
|
@ -64,7 +69,7 @@ type LocalPageInfo {
|
|||
|
||||
extend type Query {
|
||||
environmentApp(page: Int, scope: String): LocalEnvironmentApp
|
||||
folder(environment: NestedLocalEnvironmentInput): LocalEnvironmentFolder
|
||||
folder(environment: NestedLocalEnvironmentInput, scope: String): LocalEnvironmentFolder
|
||||
environmentToDelete: LocalEnvironment
|
||||
pageInfo: LocalPageInfo
|
||||
environmentToRollback: LocalEnvironment
|
||||
|
@ -82,5 +87,5 @@ extend type Mutation {
|
|||
setEnvironmentToRollback(environment: LocalEnvironmentInput): LocalErrors
|
||||
setEnvironmentToStop(environment: LocalEnvironmentInput): 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 {
|
||||
id
|
||||
lastImportTarget {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#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 {
|
||||
...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
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
||||
type LocalWorkItemAssigneesInput {
|
||||
input LocalWorkItemAssigneesInput {
|
||||
id: WorkItemID!
|
||||
assigneeIds: [ID!]
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
@import './pages/note_form';
|
||||
@import './pages/notes';
|
||||
@import './pages/notifications';
|
||||
@import './pages/pages';
|
||||
@import './pages/pipelines';
|
||||
@import './pages/profile';
|
||||
@import './pages/profiles/preferences';
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
@import 'bootstrap/scss/buttons';
|
||||
@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;
|
||||
$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 do
|
||||
push_frontend_feature_flag(:ajax_new_deploy_token, @project)
|
||||
push_frontend_feature_flag(:ci_variable_settings_graphql, @project)
|
||||
end
|
||||
|
||||
helper_method :highlight_badge
|
||||
|
|
|
@ -29,7 +29,7 @@ module InviteMembersHelper
|
|||
invalid_groups: source.related_group_ids,
|
||||
help_link: help_page_url('user/permissions'),
|
||||
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))
|
||||
end
|
||||
|
||||
|
|
|
@ -52,12 +52,6 @@ module ProjectsHelper
|
|||
content_tag(:span, username, name_tag_options)
|
||||
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)
|
||||
default_opts = { avatar: true, name: true, title: ":name" }
|
||||
opts = default_opts.merge(opts)
|
||||
|
|
|
@ -22,6 +22,7 @@ module Clusters
|
|||
|
||||
scope :ordered_by_name, -> { order(:name) }
|
||||
scope :with_name, -> (name) { where(name: name) }
|
||||
scope :has_vulnerabilities, -> (value = true) { where(has_vulnerabilities: value) }
|
||||
|
||||
validates :name,
|
||||
presence: true,
|
||||
|
|
|
@ -362,7 +362,7 @@ class Group < Namespace
|
|||
end
|
||||
|
||||
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,
|
||||
users,
|
||||
access_level,
|
||||
|
@ -374,7 +374,7 @@ class Group < Namespace
|
|||
end
|
||||
|
||||
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,
|
||||
user,
|
||||
access_level,
|
||||
|
@ -382,7 +382,7 @@ class Group < Namespace
|
|||
expires_at: expires_at,
|
||||
ldap: ldap,
|
||||
blocking_refresh: blocking_refresh
|
||||
).execute
|
||||
)
|
||||
end
|
||||
|
||||
def add_guest(user, current_user = nil)
|
||||
|
|
|
@ -31,11 +31,6 @@ class ProjectHook < WebHook
|
|||
_('Webhooks')
|
||||
end
|
||||
|
||||
override :rate_limit
|
||||
def rate_limit
|
||||
project.actual_limits.limit_for(:web_hook_calls)
|
||||
end
|
||||
|
||||
override :application_context
|
||||
def application_context
|
||||
super.merge(project: project)
|
||||
|
|
|
@ -127,19 +127,12 @@ class WebHook < ApplicationRecord
|
|||
|
||||
# @return [Boolean] Whether or not the WebHook is currently throttled.
|
||||
def rate_limited?
|
||||
return false unless rate_limit
|
||||
|
||||
Gitlab::ApplicationRateLimiter.peek(
|
||||
:web_hook_calls,
|
||||
scope: [self],
|
||||
threshold: rate_limit
|
||||
)
|
||||
rate_limiter.rate_limited?
|
||||
end
|
||||
|
||||
# Threshold for the rate-limit.
|
||||
# Overridden in ProjectHook and GroupHook, other WebHooks are not rate-limited.
|
||||
# @return [Integer] The rate limit for the WebHook. `0` for no limit.
|
||||
def rate_limit
|
||||
nil
|
||||
rate_limiter.limit
|
||||
end
|
||||
|
||||
# Returns the associated Project or Group for the WebHook if one exists.
|
||||
|
@ -180,4 +173,8 @@ class WebHook < ApplicationRecord
|
|||
def initialize_url_variables
|
||||
self.url_variables = {} if encrypted_url_variables.nil?
|
||||
end
|
||||
|
||||
def rate_limiter
|
||||
@rate_limiter ||= Gitlab::WebHooks::RateLimiter.new(self)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,6 +29,12 @@ class GroupMember < Member
|
|||
|
||||
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
|
||||
Gitlab::Access.options_with_owner
|
||||
end
|
||||
|
|
|
@ -44,7 +44,7 @@ class ProjectMember < Member
|
|||
project_ids.each do |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,
|
||||
users,
|
||||
access_level,
|
||||
|
@ -73,6 +73,16 @@ class ProjectMember < Member
|
|||
truncate_teams [project.id]
|
||||
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
|
||||
Gitlab::Access.options
|
||||
end
|
||||
|
|
|
@ -44,7 +44,7 @@ class ProjectTeam
|
|||
end
|
||||
|
||||
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,
|
||||
users,
|
||||
access_level,
|
||||
|
@ -56,12 +56,12 @@ class ProjectTeam
|
|||
end
|
||||
|
||||
def add_user(user, access_level, current_user: nil, expires_at: nil)
|
||||
Members::Projects::CreatorService.new(project, # rubocop:disable CodeReuse/ServiceClass
|
||||
user,
|
||||
access_level,
|
||||
current_user: current_user,
|
||||
expires_at: expires_at)
|
||||
.execute
|
||||
Members::Projects::CreatorService.add_user( # rubocop:disable CodeReuse/ServiceClass
|
||||
project,
|
||||
user,
|
||||
access_level,
|
||||
current_user: current_user,
|
||||
expires_at: expires_at)
|
||||
end
|
||||
|
||||
# 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
|
||||
MERGE_REQUEST_SYNC_BATCH_SIZE = 20
|
||||
MERGE_REQUEST_SYNC_BATCH_DELAY = 1.minute.freeze
|
||||
NOT_SITE_ADMIN = 'The Jira user is not a site administrator.'
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
create_subscription
|
||||
|
@ -20,7 +25,7 @@ module JiraConnectSubscriptions
|
|||
private
|
||||
|
||||
def can_administer_jira?
|
||||
@params[:jira_user]&.site_admin?
|
||||
params[:jira_user]&.site_admin?
|
||||
end
|
||||
|
||||
def create_subscription
|
||||
|
|
|
@ -12,6 +12,105 @@ module Members
|
|||
def access_levels
|
||||
Gitlab::Access.sym_options_with_owner
|
||||
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
|
||||
|
||||
def initialize(source, user, access_level, **args)
|
||||
|
@ -21,10 +120,12 @@ module Members
|
|||
@args = args
|
||||
end
|
||||
|
||||
private_class_method :new
|
||||
|
||||
def execute
|
||||
find_or_build_member
|
||||
commit_member
|
||||
create_member_task
|
||||
after_commit_tasks
|
||||
|
||||
member
|
||||
end
|
||||
|
@ -92,6 +193,10 @@ module Members
|
|||
end
|
||||
end
|
||||
|
||||
def after_commit_tasks
|
||||
create_member_task
|
||||
end
|
||||
|
||||
def create_member_task
|
||||
return unless member.persisted?
|
||||
return if member_task_attributes.value?(nil)
|
||||
|
@ -163,15 +268,19 @@ module Members
|
|||
end
|
||||
|
||||
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
|
||||
# wouldn't be found in `source.members` if it already existed
|
||||
# this of course will not treat active invites the same since we aren't searching on email
|
||||
source.members_and_requesters.find_or_initialize_by(user_id: user.id) # rubocop:disable CodeReuse/ActiveRecord
|
||||
# We have to use `members_and_requesters` here since the given `members` is modified in the models
|
||||
# to act more like a scope(removing the requested_at members) and therefore ActiveRecord has issues with that
|
||||
# on build and refreshing that relation.
|
||||
existing_members[user.id] || source.members_and_requesters.build(user_id: user.id) # rubocop:disable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
||||
def ldap
|
||||
args[:ldap] || false
|
||||
end
|
||||
|
||||
def existing_members
|
||||
args[:existing_members] || {}
|
||||
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 Groups
|
||||
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
|
||||
|
||||
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 Projects
|
||||
class CreatorService < Members::CreatorService
|
||||
class << self
|
||||
def cannot_manage_owners?(source, current_user)
|
||||
!Ability.allowed?(current_user, :manage_owners, source)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def can_create_new_member?
|
||||
|
|
|
@ -104,7 +104,7 @@ class WebHookService
|
|||
|
||||
def async_execute
|
||||
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?
|
||||
|
||||
params = {
|
||||
|
@ -215,24 +215,16 @@ class WebHookService
|
|||
string_size_limit(response_body, RESPONSE_BODY_SIZE_LIMIT)
|
||||
end
|
||||
|
||||
def rate_limited?
|
||||
return false if rate_limit.nil?
|
||||
|
||||
Gitlab::ApplicationRateLimiter.throttled?(
|
||||
:web_hook_calls,
|
||||
scope: [hook],
|
||||
threshold: rate_limit
|
||||
)
|
||||
# Increments rate-limit counter.
|
||||
# Returns true if hook should be rate-limited.
|
||||
def rate_limit!
|
||||
Gitlab::WebHooks::RateLimiter.new(hook).rate_limit!
|
||||
end
|
||||
|
||||
def recursion_blocked?
|
||||
Gitlab::WebHooks::RecursionDetection.block?(hook)
|
||||
end
|
||||
|
||||
def rate_limit
|
||||
@rate_limit ||= hook.rate_limit
|
||||
end
|
||||
|
||||
def log_rate_limited
|
||||
log_auth_error('Webhook rate limit exceeded')
|
||||
end
|
||||
|
|
|
@ -11,6 +11,6 @@
|
|||
%p
|
||||
= s_('403|Please contact your GitLab administrator to get permission.')
|
||||
.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')
|
||||
= render "errors/footer"
|
||||
|
|
|
@ -52,5 +52,4 @@
|
|||
.settings-content
|
||||
= 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)
|
||||
|
||||
.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)) }
|
||||
|
|
|
@ -4,20 +4,21 @@
|
|||
.card
|
||||
.card-header
|
||||
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|
|
||||
%li.pages-domain-list-item.list-group-item.d-flex.justify-content-between
|
||||
- if verification_enabled
|
||||
- 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 }
|
||||
= sprite_icon("status_#{status}" )
|
||||
.domain-name
|
||||
= external_link(domain.url, domain.url)
|
||||
- if domain.certificate
|
||||
%div
|
||||
= gl_badge_tag(s_('GitLabPages|Certificate: %{subject}') % { subject: domain.pages_domain.subject })
|
||||
- if domain.expired?
|
||||
= gl_badge_tag s_('GitLabPages|Expired'), variant: :danger
|
||||
%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
|
||||
- tooltip, status = domain.unverified? ? [s_('GitLabPages|Unverified'), 'failed'] : [s_('GitLabPages|Verified'), 'success']
|
||||
.domain-status.ci-status-icon.has-tooltip{ class: "gl-mr-5 ci-status-icon-#{status}", title: tooltip }
|
||||
= sprite_icon("status_#{status}" )
|
||||
.domain-name
|
||||
= external_link(domain.url, domain.url)
|
||||
- if domain.certificate
|
||||
%div
|
||||
= gl_badge_tag(s_('GitLabPages|Certificate: %{subject}') % { subject: domain.pages_domain.subject })
|
||||
- if domain.expired?
|
||||
= gl_badge_tag s_('GitLabPages|Expired'), variant: :danger
|
||||
%div
|
||||
= link_to s_('GitLabPages|Edit'), project_pages_domain_path(@project, domain), class: "btn gl-button btn-sm btn-grouped btn-confirm btn-inverted"
|
||||
= link_to s_('GitLabPages|Remove'), project_pages_domain_path(@project, domain), data: { confirm: s_('GitLabPages|Are you sure?'), 'confirm-btn-variant': 'danger'}, "aria-label": s_("GitLabPages|Remove domain"), method: :delete, class: "btn gl-button btn-danger btn-sm btn-grouped"
|
||||
|
|
|
@ -19,10 +19,10 @@
|
|||
.col-sm-2
|
||||
= _("Verification status")
|
||||
.col-sm-10
|
||||
.status-badge
|
||||
.gl-mb-3
|
||||
- text, status = domain_presenter.unverified? ? [_('Unverified'), :danger] : [_('Verified'), :success]
|
||||
= 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
|
||||
= text_field_tag :domain_verification, verification_record, class: "monospace js-select-on-focus form-control", readonly: true
|
||||
.input-group-append
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
resource: @project,
|
||||
token: @resource_access_token,
|
||||
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,
|
||||
prefix: :resource_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
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88506
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/363450
|
||||
name: ci_variable_settings_graphql
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89332
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/364423
|
||||
milestone: '15.1'
|
||||
type: development
|
||||
group: group::release
|
||||
group: group::pipeline authoring
|
||||
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,
|
||||
name text NOT NULL,
|
||||
created_by_user_id bigint,
|
||||
has_vulnerabilities boolean DEFAULT false NOT NULL,
|
||||
CONSTRAINT check_3498369510 CHECK ((char_length(name) <= 255))
|
||||
);
|
||||
|
||||
|
@ -13795,6 +13796,8 @@ CREATE TABLE compliance_management_frameworks (
|
|||
color text NOT NULL,
|
||||
namespace_id integer NOT NULL,
|
||||
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_1617e0b87e CHECK ((char_length(description) <= 255)),
|
||||
CONSTRAINT check_ab00bc2193 CHECK ((char_length(name) <= 255)),
|
||||
|
@ -18780,7 +18783,9 @@ CREATE TABLE plan_limits (
|
|||
pipeline_triggers integer DEFAULT 25000 NOT NULL,
|
||||
project_ci_secure_files integer DEFAULT 100 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
|
||||
|
@ -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 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_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_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_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.
|
||||
> - [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.
|
||||
|
||||
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`)
|
||||
- Owner (`50`) - Only valid to set for groups
|
||||
|
||||
WARNING:
|
||||
Due to [an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/219299),
|
||||
projects in personal namespaces don't show owner (`50`) permission.
|
||||
NOTE:
|
||||
From [GitLab 14.9](https://gitlab.com/gitlab-org/gitlab/-/issues/351211) and later, projects have a maximum role of Owner.
|
||||
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
|
||||
|
||||
|
|
|
@ -233,7 +233,7 @@ To protect a group-level environment, make sure your environments have the corre
|
|||
|
||||
#### 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 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
|
||||
|
||||
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 |
|
||||
|----------------------|-------------------------|
|
||||
| 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 |
|
||||
| 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.
|
||||
|
||||
![SAML Group Links](img/saml_group_links_v13_9.png)
|
||||
|
||||
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:
|
||||
1. The user data store on the identity provider or the list of users on the specific app.
|
||||
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).
|
||||
|
||||
|
@ -402,151 +402,7 @@ For example, to unlink the `MyOrg` account:
|
|||
|
||||
## Group Sync
|
||||
|
||||
WARNING:
|
||||
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.
|
||||
|
||||
![SAML Group Links](img/saml_group_links_v13_9.png)
|
||||
|
||||
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
|
||||
```
|
||||
For information on automatically managing GitLab group membership, see [SAML Group Sync](group_sync.md).
|
||||
|
||||
## Passwords for users created via SAML SSO for Groups
|
||||
|
||||
|
|
|
@ -30,6 +30,8 @@ module Atlassian
|
|||
responses.compact
|
||||
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)
|
||||
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 },
|
||||
profile_add_new_email: { threshold: 5, 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 },
|
||||
username_exists: { 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"
|
||||
msgstr ""
|
||||
|
||||
msgid "JiraConnect|Cannot find namespace. Make sure you have sufficient permissions."
|
||||
msgstr ""
|
||||
|
||||
msgid "JiraConnect|Configure your Jira Connect Application ID."
|
||||
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}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -21763,6 +21769,9 @@ msgstr ""
|
|||
msgid "JiraConnect|New branch was successfully created."
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ module QA
|
|||
element :ci_variable_delete_button
|
||||
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 :add_ci_variable_button
|
||||
element :edit_ci_variable_button
|
||||
|
|
|
@ -12,8 +12,18 @@ RSpec.describe 'Group variables', :js do
|
|||
group.add_owner(user)
|
||||
gitlab_sign_in(user)
|
||||
wait_for_requests
|
||||
visit page_path
|
||||
end
|
||||
|
||||
it_behaves_like 'variable list'
|
||||
context 'with disabled ff `ci_variable_settings_graphql' do
|
||||
before do
|
||||
stub_feature_flags(ci_variable_settings_graphql: false)
|
||||
visit page_path
|
||||
end
|
||||
|
||||
it_behaves_like 'variable list'
|
||||
end
|
||||
|
||||
# TODO: Uncomment when the new graphQL app for variable settings
|
||||
# is enabled.
|
||||
# it_behaves_like 'variable list'
|
||||
end
|
||||
|
|
|
@ -12,28 +12,36 @@ RSpec.describe 'Project variables', :js do
|
|||
sign_in(user)
|
||||
project.add_maintainer(user)
|
||||
project.variables << variable
|
||||
visit page_path
|
||||
end
|
||||
|
||||
it_behaves_like 'variable list'
|
||||
|
||||
it 'adds a new variable with an environment scope' do
|
||||
click_button('Add variable')
|
||||
|
||||
page.within('#add-ci-variable') do
|
||||
fill_in 'Key', with: 'akey'
|
||||
find('#ci-variable-value').set('akey_value')
|
||||
find('[data-testid="environment-scope"]').click
|
||||
find('[data-testid="ci-environment-search"]').set('review/*')
|
||||
find('[data-testid="create-wildcard-button"]').click
|
||||
|
||||
click_button('Add variable')
|
||||
# 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
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
it_behaves_like 'variable list'
|
||||
|
||||
page.within('[data-testid="ci-variable-table"]') do
|
||||
expect(find('.js-ci-variable-row:first-child [data-label="Environments"]').text).to eq('review/*')
|
||||
it 'adds a new variable with an environment scope' do
|
||||
click_button('Add variable')
|
||||
|
||||
page.within('#add-ci-variable') do
|
||||
fill_in 'Key', with: 'akey'
|
||||
find('#ci-variable-value').set('akey_value')
|
||||
find('[data-testid="environment-scope"]').click
|
||||
find('[data-testid="ci-environment-search"]').set('review/*')
|
||||
find('[data-testid="create-wildcard-button"]').click
|
||||
|
||||
click_button('Add variable')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
page.within('[data-testid="ci-variable-table"]') do
|
||||
expect(find('.js-ci-variable-row:first-child [data-label="Environments"]').text).to eq('review/*')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -48,20 +48,48 @@ RSpec.describe 'Projects > Members > Manage members', :js do
|
|||
end
|
||||
end
|
||||
|
||||
it 'uses ProjectMember access_level_roles for the invite members modal access option', :aggregate_failures do
|
||||
visit_members_page
|
||||
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 'Invite members'
|
||||
|
||||
click_on 'Guest'
|
||||
wait_for_requests
|
||||
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).not_to have_button('Owner')
|
||||
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
|
||||
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).not_to have_button('Owner')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
|
|||
import { mount } from '@vue/test-utils';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
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);
|
||||
|
||||
|
@ -20,7 +20,7 @@ describe('Ci environments dropdown', () => {
|
|||
},
|
||||
});
|
||||
|
||||
wrapper = mount(CiEnvironmentsDropdown, {
|
||||
wrapper = mount(LegacyCiEnvironmentsDropdown, {
|
||||
store,
|
||||
propsData: {
|
||||
value: term,
|
|
@ -4,7 +4,7 @@ import Vue from 'vue';
|
|||
import Vuex from 'vuex';
|
||||
import { mockTracking } from 'helpers/tracking_helper';
|
||||
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 {
|
||||
AWS_ACCESS_KEY_ID,
|
||||
EVENT_LABEL,
|
||||
|
@ -30,7 +30,7 @@ describe('Ci variable modal', () => {
|
|||
isGroup: options.isGroup,
|
||||
environmentScopeLink: '/help/environments',
|
||||
});
|
||||
wrapper = method(CiVariableModal, {
|
||||
wrapper = method(LegacyCiVariableModal, {
|
||||
attachTo: document.body,
|
||||
stubs: {
|
||||
GlModal: ModalStub,
|
|
@ -1,7 +1,7 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
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';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
@ -15,7 +15,7 @@ describe('Ci variable table', () => {
|
|||
store = createStore();
|
||||
store.state.isGroup = groupState;
|
||||
jest.spyOn(store, 'dispatch').mockImplementation();
|
||||
wrapper = shallowMount(CiVariableSettings, {
|
||||
wrapper = shallowMount(LegacyCiVariableSettings, {
|
||||
store,
|
||||
});
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
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 mockData from '../services/mock_data';
|
||||
|
||||
|
@ -14,7 +14,7 @@ describe('Ci variable table', () => {
|
|||
const createComponent = () => {
|
||||
store = createStore();
|
||||
jest.spyOn(store, 'dispatch').mockImplementation();
|
||||
wrapper = mountExtended(CiVariableTable, {
|
||||
wrapper = mountExtended(LegacyCiVariableTable, {
|
||||
attachTo: document.body,
|
||||
store,
|
||||
});
|
|
@ -355,30 +355,6 @@ RSpec.describe ProjectsHelper do
|
|||
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
|
||||
context 'when user is not logged in and gitlab protocol is HTTP' do
|
||||
it 'returns HTTP' do
|
||||
|
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe Atlassian::JiraConnect::Client do
|
||||
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(: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)
|
||||
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
|
||||
|
|
|
@ -77,6 +77,7 @@ RSpec.describe Gitlab::DatabaseImporters::InstanceAdministrators::CreateGroup do
|
|||
create(:user)
|
||||
|
||||
expect(result[:status]).to eq(:success)
|
||||
group.reset
|
||||
expect(group.members.collect(&:user)).to contain_exactly(user, admin1, admin2)
|
||||
expect(group.members.collect(&:access_level)).to contain_exactly(
|
||||
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) }
|
||||
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
|
||||
|
||||
describe 'validation' do
|
||||
|
|
|
@ -31,15 +31,6 @@ RSpec.describe ProjectHook do
|
|||
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
|
||||
it 'returns the associated project' do
|
||||
project = build(:project)
|
||||
|
|
|
@ -23,14 +23,6 @@ RSpec.describe ServiceHook do
|
|||
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
|
||||
let(:hook) { build(:service_hook, integration: integration) }
|
||||
|
||||
|
|
|
@ -185,14 +185,6 @@ RSpec.describe SystemHook do
|
|||
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
|
||||
let(:hook) { build(:system_hook) }
|
||||
|
||||
|
|
|
@ -493,31 +493,30 @@ RSpec.describe WebHook do
|
|||
end
|
||||
|
||||
describe '#rate_limited?' do
|
||||
context 'when there are rate limits' do
|
||||
before do
|
||||
allow(hook).to receive(:rate_limit).and_return(3)
|
||||
it 'is false when hook has not been rate limited' do
|
||||
expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
|
||||
expect(rate_limiter).to receive(:rate_limited?).and_return(false)
|
||||
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
|
||||
end
|
||||
|
||||
it 'is true when hook has been rate limited' do
|
||||
expect(Gitlab::ApplicationRateLimiter).to receive(:peek).and_return(true)
|
||||
expect(hook).to be_rate_limited
|
||||
end
|
||||
expect(hook).not_to be_rate_limited
|
||||
end
|
||||
|
||||
context 'when there are no rate limits' do
|
||||
before do
|
||||
allow(hook).to receive(:rate_limit).and_return(nil)
|
||||
it 'is true when hook has been rate limited' do
|
||||
expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
|
||||
expect(rate_limiter).to receive(:rate_limited?).and_return(true)
|
||||
end
|
||||
|
||||
it 'does not call Gitlab::ApplicationRateLimiter, and is false' do
|
||||
expect(Gitlab::ApplicationRateLimiter).not_to receive(:peek)
|
||||
expect(hook).not_to be_rate_limited
|
||||
expect(hook).to be_rate_limited
|
||||
end
|
||||
end
|
||||
|
||||
describe '#rate_limit' do
|
||||
it 'returns the hook rate limit' do
|
||||
expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
|
||||
expect(rate_limiter).to receive(:limit).and_return(10)
|
||||
end
|
||||
|
||||
expect(hook.rate_limit).to eq(10)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -47,6 +47,16 @@ RSpec.describe GroupMember do
|
|||
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
|
||||
|
||||
describe '#namespace_id' do
|
||||
|
|
|
@ -23,6 +23,30 @@ RSpec.describe ProjectMember do
|
|||
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
|
||||
subject { create(:project_member).real_source_type }
|
||||
|
||||
|
|
|
@ -213,6 +213,8 @@ RSpec.describe PlanLimits do
|
|||
storage_size_limit
|
||||
daily_invites
|
||||
web_hook_calls
|
||||
web_hook_calls_mid
|
||||
web_hook_calls_low
|
||||
ci_daily_pipeline_schedule_triggers
|
||||
repository_size
|
||||
security_policy_scan_execution_schedules
|
||||
|
|
|
@ -59,13 +59,13 @@ RSpec.describe API::Invitations do
|
|||
|
||||
context 'when authenticated as a maintainer/owner' 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
|
||||
post invitations_url(source, maintainer),
|
||||
params: { email: access_requester.email, access_level: Member::MAINTAINER }
|
||||
|
||||
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
|
||||
|
||||
|
@ -258,12 +258,13 @@ RSpec.describe API::Invitations do
|
|||
end
|
||||
end
|
||||
|
||||
it "returns a message if member already exists" do
|
||||
it "updates an already existing active member" do
|
||||
post invitations_url(source, maintainer),
|
||||
params: { email: developer.email, access_level: Member::MAINTAINER }
|
||||
|
||||
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
|
||||
|
||||
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'
|
||||
|
||||
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
|
||||
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'
|
||||
|
||||
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
|
||||
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'
|
||||
|
||||
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
|
||||
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'
|
||||
|
||||
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
|
||||
post invitations_url(group, maintainer), params: { email: emails, access_level: Member::DEVELOPER }
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe JiraConnectSubscriptions::CreateService do
|
||||
let(:installation) { create(:jira_connect_installation) }
|
||||
let(:current_user) { create(:user) }
|
||||
let(:group) { create(:group) }
|
||||
let_it_be(:installation) { create(:jira_connect_installation) }
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
let(:path) { group.full_path }
|
||||
let(:params) { { namespace_path: path, jira_user: jira_user } }
|
||||
let(:jira_user) { double(:JiraUser, site_admin?: true) }
|
||||
|
@ -16,38 +17,31 @@ RSpec.describe JiraConnectSubscriptions::CreateService do
|
|||
group.add_maintainer(current_user)
|
||||
end
|
||||
|
||||
shared_examples 'a failed execution' do
|
||||
shared_examples 'a failed execution' do |**status_attributes|
|
||||
it 'does not create a subscription' do
|
||||
expect { subject }.not_to change { installation.subscriptions.count }
|
||||
end
|
||||
|
||||
it 'returns an error status' do
|
||||
expect(subject[:status]).to eq(:error)
|
||||
expect(subject).to include(status_attributes)
|
||||
end
|
||||
end
|
||||
|
||||
context 'remote user does not have access' do
|
||||
let(:jira_user) { double(site_admin?: false) }
|
||||
|
||||
it 'does not create a subscription' do
|
||||
expect { subject }.not_to change { installation.subscriptions.count }
|
||||
end
|
||||
|
||||
it 'returns error' do
|
||||
expect(subject[:status]).to eq(:error)
|
||||
end
|
||||
it_behaves_like 'a failed execution',
|
||||
http_status: 403,
|
||||
message: 'The Jira user is not a site administrator. Check the permissions in Jira and try again.'
|
||||
end
|
||||
|
||||
context 'remote user cannot be retrieved' do
|
||||
let(:jira_user) { nil }
|
||||
|
||||
it 'does not create a subscription' do
|
||||
expect { subject }.not_to change { installation.subscriptions.count }
|
||||
end
|
||||
|
||||
it 'returns error' do
|
||||
expect(subject[:status]).to eq(:error)
|
||||
end
|
||||
it_behaves_like 'a failed execution',
|
||||
http_status: 403,
|
||||
message: 'Could not fetch user information from Jira. Check the permissions in Jira and try again.'
|
||||
end
|
||||
|
||||
context 'when user does have access' do
|
||||
|
@ -60,8 +54,8 @@ RSpec.describe JiraConnectSubscriptions::CreateService do
|
|||
end
|
||||
|
||||
context 'namespace has projects' do
|
||||
let!(:project_1) { create(:project, group: group) }
|
||||
let!(:project_2) { create(:project, group: group) }
|
||||
let_it_be(:project_1) { create(:project, group: group) }
|
||||
let_it_be(:project_2) { create(:project, group: group) }
|
||||
|
||||
before do
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
|
|
|
@ -11,7 +11,7 @@ RSpec.describe Members::CreatorService do
|
|||
describe '#execute' do
|
||||
it 'raises error for new member on authorization check implementation' 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
|
||||
|
||||
|
@ -19,7 +19,7 @@ RSpec.describe Members::CreatorService do
|
|||
source.add_developer(user)
|
||||
|
||||
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
|
||||
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'
|
||||
|
||||
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
|
||||
it 'returns Gitlab::Access.options_with_owner' do
|
||||
expect(described_class.access_levels).to eq(Gitlab::Access.sym_options_with_owner)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
let_it_be(:source, reload: true) { create(:group, :public) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
it_behaves_like 'owner management'
|
||||
|
||||
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
|
||||
let_it_be(:member_type) { GroupMember }
|
||||
end
|
||||
|
@ -22,7 +30,7 @@ RSpec.describe Members::Groups::CreatorService do
|
|||
expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait).once
|
||||
|
||||
1.upto(3) do
|
||||
described_class.new(source, user, :maintainer).execute
|
||||
described_class.add_user(source, user, :maintainer)
|
||||
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
|
||||
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
|
||||
expect_not_to_create_members
|
||||
expect(result[:message][existing_member.user.email]).to eq("User already exists in source")
|
||||
it 'allows re-invite of an already invited email and updates the access_level' do
|
||||
expect { result }.not_to change(ProjectMember, :count)
|
||||
expect(result[:status]).to eq(:success)
|
||||
expect(existing_member.reset.access_level).to eq ProjectMember::MAINTAINER
|
||||
end
|
||||
|
||||
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(:params) { { email: "#{secondary_email.email}" } }
|
||||
|
||||
it 'returns an error for the already invited email' do
|
||||
expect_not_to_create_members
|
||||
expect(result[:message][secondary_email.email]).to eq("User already exists in source")
|
||||
it 'allows re-invite to an already invited email' do
|
||||
expect_to_create_members(count: 0)
|
||||
expect(result[:status]).to eq(:success)
|
||||
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'
|
||||
|
||||
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
|
||||
it 'returns Gitlab::Access.sym_options_with_owner' do
|
||||
expect(described_class.access_levels).to eq(Gitlab::Access.sym_options_with_owner)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
let_it_be(:source, reload: true) { create(:project, :public) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
it_behaves_like 'owner management'
|
||||
|
||||
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
|
||||
let_it_be(:member_type) { ProjectMember }
|
||||
end
|
||||
|
@ -22,7 +30,7 @@ RSpec.describe Members::Projects::CreatorService do
|
|||
expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to receive(:bulk_perform_in).once
|
||||
|
||||
1.upto(3) do
|
||||
described_class.new(source, user, :maintainer).execute
|
||||
described_class.add_user(source, user, :maintainer)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue