Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
da6cd333e7
commit
02c6800ac5
|
@ -1,10 +1,66 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlIcon } from '@gitlab/ui';
|
import { GlAccordion, GlAccordionItem, GlBadge, GlIcon } from '@gitlab/ui';
|
||||||
import STATUS_MAP from '../constants';
|
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||||
|
import { __, s__ } from '~/locale';
|
||||||
|
import { STATUSES } from '../constants';
|
||||||
|
|
||||||
|
const STATISTIC_ITEMS = {
|
||||||
|
diff_note: __('Diff notes'),
|
||||||
|
issue: __('Issues'),
|
||||||
|
label: __('Labels'),
|
||||||
|
milestone: __('Milestones'),
|
||||||
|
note: __('Notes'),
|
||||||
|
pull_request: s__('GithubImporter|Pull requests'),
|
||||||
|
pull_request_merged_by: s__('GithubImporter|PR mergers'),
|
||||||
|
pull_request_review: s__('GithubImporter|PR reviews'),
|
||||||
|
release: __('Releases'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// support both camel case and snake case versions
|
||||||
|
Object.assign(STATISTIC_ITEMS, convertObjectPropsToCamelCase(STATISTIC_ITEMS));
|
||||||
|
|
||||||
|
const SCHEDULED_STATUS = {
|
||||||
|
icon: 'status-scheduled',
|
||||||
|
text: __('Pending'),
|
||||||
|
variant: 'muted',
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATUS_MAP = {
|
||||||
|
[STATUSES.NONE]: {
|
||||||
|
icon: 'status-waiting',
|
||||||
|
text: __('Not started'),
|
||||||
|
variant: 'muted',
|
||||||
|
},
|
||||||
|
[STATUSES.SCHEDULING]: SCHEDULED_STATUS,
|
||||||
|
[STATUSES.SCHEDULED]: SCHEDULED_STATUS,
|
||||||
|
[STATUSES.CREATED]: SCHEDULED_STATUS,
|
||||||
|
[STATUSES.STARTED]: {
|
||||||
|
icon: 'status-running',
|
||||||
|
text: __('Importing...'),
|
||||||
|
variant: 'info',
|
||||||
|
},
|
||||||
|
[STATUSES.FAILED]: {
|
||||||
|
icon: 'status-failed',
|
||||||
|
text: __('Failed'),
|
||||||
|
variant: 'danger',
|
||||||
|
},
|
||||||
|
[STATUSES.CANCELLED]: {
|
||||||
|
icon: 'status-stopped',
|
||||||
|
text: __('Cancelled'),
|
||||||
|
variant: 'neutral',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function isIncompleteImport(stats) {
|
||||||
|
return Object.keys(stats.fetched).some((key) => stats.fetched[key] !== stats.imported[key]);
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ImportStatus',
|
name: 'ImportStatus',
|
||||||
components: {
|
components: {
|
||||||
|
GlAccordion,
|
||||||
|
GlAccordionItem,
|
||||||
|
GlBadge,
|
||||||
GlIcon,
|
GlIcon,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
@ -12,19 +68,88 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
stats: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({ fetched: {}, imported: {} }),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
knownStats() {
|
||||||
|
const knownStatisticKeys = Object.keys(STATISTIC_ITEMS);
|
||||||
|
return Object.keys(this.stats.fetched).filter((key) => knownStatisticKeys.includes(key));
|
||||||
|
},
|
||||||
|
|
||||||
|
hasStats() {
|
||||||
|
return this.stats && this.knownStats.length > 0;
|
||||||
|
},
|
||||||
|
|
||||||
mappedStatus() {
|
mappedStatus() {
|
||||||
|
if (this.status === STATUSES.FINISHED) {
|
||||||
|
const isIncomplete = this.stats && isIncompleteImport(this.stats);
|
||||||
|
return {
|
||||||
|
icon: 'status-success',
|
||||||
|
...(isIncomplete
|
||||||
|
? {
|
||||||
|
text: __('Partial import'),
|
||||||
|
variant: 'warning',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
text: __('Complete'),
|
||||||
|
variant: 'success',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return STATUS_MAP[this.status];
|
return STATUS_MAP[this.status];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getStatisticIconProps(key) {
|
||||||
|
const fetched = this.stats.fetched[key];
|
||||||
|
const imported = this.stats.imported[key];
|
||||||
|
|
||||||
|
if (fetched === imported) {
|
||||||
|
return { name: 'status-success', class: 'gl-text-green-400' };
|
||||||
|
} else if (imported === 0) {
|
||||||
|
return { name: 'status-scheduled', class: 'gl-text-gray-400' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { name: 'status-running', class: 'gl-text-blue-400' };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
STATISTIC_ITEMS,
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<gl-icon :name="mappedStatus.icon" :class="mappedStatus.iconClass" :size="12" class="gl-mr-2" />
|
<div class="gl-display-inline-block gl-w-13">
|
||||||
<span>{{ mappedStatus.text }}</span>
|
<gl-badge :icon="mappedStatus.icon" :variant="mappedStatus.variant" size="md" class="gl-mr-2">
|
||||||
|
{{ mappedStatus.text }}
|
||||||
|
</gl-badge>
|
||||||
|
</div>
|
||||||
|
<gl-accordion v-if="hasStats" :header-level="3">
|
||||||
|
<gl-accordion-item :title="__('Details')">
|
||||||
|
<ul class="gl-p-0 gl-list-style-none gl-font-sm">
|
||||||
|
<li v-for="key in knownStats" :key="key">
|
||||||
|
<div class="gl-display-flex gl-w-20 gl-align-items-center">
|
||||||
|
<gl-icon
|
||||||
|
:size="12"
|
||||||
|
class="gl-mr-3 gl-flex-shrink-0"
|
||||||
|
v-bind="getStatisticIconProps(key)"
|
||||||
|
/>
|
||||||
|
<span class="">{{ $options.STATISTIC_ITEMS[key] }}</span>
|
||||||
|
<span class="gl-ml-auto">
|
||||||
|
{{ stats.imported[key] || 0 }}/{{ stats.fetched[key] }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</gl-accordion-item>
|
||||||
|
</gl-accordion>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import { __ } from '~/locale';
|
|
||||||
|
|
||||||
// The `scheduling` status is only present on the client-side,
|
// The `scheduling` status is only present on the client-side,
|
||||||
// it is used as the status when we are requesting to start an import.
|
// it is used as the status when we are requesting to start an import.
|
||||||
|
|
||||||
|
@ -13,42 +11,3 @@ export const STATUSES = {
|
||||||
SCHEDULING: 'scheduling',
|
SCHEDULING: 'scheduling',
|
||||||
CANCELLED: 'cancelled',
|
CANCELLED: 'cancelled',
|
||||||
};
|
};
|
||||||
|
|
||||||
const SCHEDULED_STATUS = {
|
|
||||||
icon: 'status-scheduled',
|
|
||||||
text: __('Pending'),
|
|
||||||
iconClass: 'gl-text-orange-400',
|
|
||||||
};
|
|
||||||
|
|
||||||
const STATUS_MAP = {
|
|
||||||
[STATUSES.NONE]: {
|
|
||||||
icon: 'status-waiting',
|
|
||||||
text: __('Not started'),
|
|
||||||
iconClass: 'gl-text-gray-400',
|
|
||||||
},
|
|
||||||
[STATUSES.SCHEDULING]: SCHEDULED_STATUS,
|
|
||||||
[STATUSES.SCHEDULED]: SCHEDULED_STATUS,
|
|
||||||
[STATUSES.CREATED]: SCHEDULED_STATUS,
|
|
||||||
[STATUSES.STARTED]: {
|
|
||||||
icon: 'status-running',
|
|
||||||
text: __('Importing...'),
|
|
||||||
iconClass: 'gl-text-blue-400',
|
|
||||||
},
|
|
||||||
[STATUSES.FINISHED]: {
|
|
||||||
icon: 'status-success',
|
|
||||||
text: __('Complete'),
|
|
||||||
iconClass: 'gl-text-green-400',
|
|
||||||
},
|
|
||||||
[STATUSES.FAILED]: {
|
|
||||||
icon: 'status-failed',
|
|
||||||
text: __('Failed'),
|
|
||||||
iconClass: 'gl-text-red-600',
|
|
||||||
},
|
|
||||||
[STATUSES.CANCELLED]: {
|
|
||||||
icon: 'status-stopped',
|
|
||||||
text: __('Cancelled'),
|
|
||||||
iconClass: 'gl-text-red-600',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default STATUS_MAP;
|
|
||||||
|
|
|
@ -69,6 +69,10 @@ export default {
|
||||||
return getImportStatus(this.repo);
|
return getImportStatus(this.repo);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
stats() {
|
||||||
|
return this.repo.importedProject?.stats;
|
||||||
|
},
|
||||||
|
|
||||||
importTarget() {
|
importTarget() {
|
||||||
return this.getImportTarget(this.repo.importSource.id);
|
return this.getImportTarget(this.repo.importSource.id);
|
||||||
},
|
},
|
||||||
|
@ -101,11 +105,11 @@ export default {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<tr
|
<tr
|
||||||
class="gl-h-11 gl-border-0 gl-border-solid gl-border-t-1 gl-border-gray-100 gl-h-11"
|
class="gl-h-11 gl-border-0 gl-border-solid gl-border-t-1 gl-border-gray-100 gl-h-11 gl-vertical-align-top"
|
||||||
data-qa-selector="project_import_row"
|
data-qa-selector="project_import_row"
|
||||||
:data-qa-source-project="repo.importSource.fullName"
|
:data-qa-source-project="repo.importSource.fullName"
|
||||||
>
|
>
|
||||||
<td class="gl-p-4">
|
<td class="gl-p-4 gl-vertical-align-top">
|
||||||
<gl-link :href="repo.importSource.providerLink" target="_blank" data-testid="providerLink"
|
<gl-link :href="repo.importSource.providerLink" target="_blank" data-testid="providerLink"
|
||||||
>{{ repo.importSource.fullName }}
|
>{{ repo.importSource.fullName }}
|
||||||
<gl-icon v-if="repo.importSource.providerLink" name="external-link" />
|
<gl-icon v-if="repo.importSource.providerLink" name="external-link" />
|
||||||
|
@ -156,10 +160,10 @@ export default {
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="repo.importedProject">{{ displayFullPath }}</template>
|
<template v-else-if="repo.importedProject">{{ displayFullPath }}</template>
|
||||||
</td>
|
</td>
|
||||||
<td class="gl-p-4" data-qa-selector="import_status_indicator">
|
<td class="gl-p-4 gl-vertical-align-top" data-qa-selector="import_status_indicator">
|
||||||
<import-status :status="importStatus" />
|
<import-status :status="importStatus" :stats="stats" />
|
||||||
</td>
|
</td>
|
||||||
<td data-testid="actions">
|
<td data-testid="actions" class="gl-vertical-align-top gl-pt-4">
|
||||||
<gl-button
|
<gl-button
|
||||||
v-if="isFinished"
|
v-if="isFinished"
|
||||||
class="btn btn-default"
|
class="btn btn-default"
|
||||||
|
|
|
@ -113,7 +113,11 @@ export default {
|
||||||
updatedProjects.forEach((updatedProject) => {
|
updatedProjects.forEach((updatedProject) => {
|
||||||
const repo = state.repositories.find((p) => p.importedProject?.id === updatedProject.id);
|
const repo = state.repositories.find((p) => p.importedProject?.id === updatedProject.id);
|
||||||
if (repo?.importedProject) {
|
if (repo?.importedProject) {
|
||||||
repo.importedProject.importStatus = updatedProject.importStatus;
|
repo.importedProject = {
|
||||||
|
...repo.importedProject,
|
||||||
|
stats: updatedProject.stats,
|
||||||
|
importStatus: updatedProject.importStatus,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -94,6 +94,7 @@ export default {
|
||||||
v-if="shouldShowAlert"
|
v-if="shouldShowAlert"
|
||||||
:variant="alert.variant"
|
:variant="alert.variant"
|
||||||
:title="alert.title"
|
:title="alert.title"
|
||||||
|
class="gl-mb-5"
|
||||||
data-testid="jira-connect-persisted-alert"
|
data-testid="jira-connect-persisted-alert"
|
||||||
@dismiss="setAlert"
|
@dismiss="setAlert"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlButton, GlTable } from '@gitlab/ui';
|
import { GlButton, GlTableLite } from '@gitlab/ui';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import { mapMutations } from 'vuex';
|
import { mapMutations } from 'vuex';
|
||||||
import { removeSubscription } from '~/jira_connect/subscriptions/api';
|
import { removeSubscription } from '~/jira_connect/subscriptions/api';
|
||||||
|
@ -12,7 +12,7 @@ import GroupItemName from './group_item_name.vue';
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
GlButton,
|
GlButton,
|
||||||
GlTable,
|
GlTableLite,
|
||||||
GroupItemName,
|
GroupItemName,
|
||||||
TimeagoTooltip,
|
TimeagoTooltip,
|
||||||
},
|
},
|
||||||
|
@ -78,7 +78,7 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<gl-table :items="subscriptions" :fields="$options.fields">
|
<gl-table-lite :items="subscriptions" :fields="$options.fields">
|
||||||
<template #cell(name)="{ item }">
|
<template #cell(name)="{ item }">
|
||||||
<group-item-name :group="item.group" />
|
<group-item-name :group="item.group" />
|
||||||
</template>
|
</template>
|
||||||
|
@ -95,5 +95,5 @@ export default {
|
||||||
>{{ __('Unlink') }}</gl-button
|
>{{ __('Unlink') }}</gl-button
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
</gl-table>
|
</gl-table-lite>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlSearchBoxByType } from '@gitlab/ui';
|
import { GlSearchBoxByType } from '@gitlab/ui';
|
||||||
import { uniq, escapeRegExp } from 'lodash';
|
import { escapeRegExp } from 'lodash';
|
||||||
import {
|
import {
|
||||||
EXCLUDED_NODES,
|
EXCLUDED_NODES,
|
||||||
HIDE_CLASS,
|
HIDE_CLASS,
|
||||||
|
@ -60,41 +60,42 @@ const hideSectionsExcept = (sectionSelector, visibleSections) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const transformMatchElement = (element, searchTerm) => {
|
const highlightTextNode = (textNode, searchTerm) => {
|
||||||
const textStr = element.textContent;
|
|
||||||
const escapedSearchTerm = new RegExp(`(${escapeRegExp(searchTerm)})`, 'gi');
|
const escapedSearchTerm = new RegExp(`(${escapeRegExp(searchTerm)})`, 'gi');
|
||||||
|
const textList = textNode.data.split(escapedSearchTerm);
|
||||||
|
|
||||||
|
return textList.reduce((documentFragment, text) => {
|
||||||
|
let addElement;
|
||||||
|
|
||||||
const textList = textStr.split(escapedSearchTerm);
|
|
||||||
const replaceFragment = document.createDocumentFragment();
|
|
||||||
textList.forEach((text) => {
|
|
||||||
let addElement = document.createTextNode(text);
|
|
||||||
if (escapedSearchTerm.test(text)) {
|
if (escapedSearchTerm.test(text)) {
|
||||||
addElement = document.createElement('mark');
|
addElement = document.createElement('mark');
|
||||||
addElement.className = `${HIGHLIGHT_CLASS} ${NONE_PADDING_CLASS}`;
|
addElement.className = `${HIGHLIGHT_CLASS} ${NONE_PADDING_CLASS}`;
|
||||||
addElement.textContent = text;
|
addElement.textContent = text;
|
||||||
escapedSearchTerm.lastIndex = 0;
|
escapedSearchTerm.lastIndex = 0;
|
||||||
|
} else {
|
||||||
|
addElement = document.createTextNode(text);
|
||||||
}
|
}
|
||||||
replaceFragment.appendChild(addElement);
|
|
||||||
});
|
|
||||||
|
|
||||||
return replaceFragment;
|
documentFragment.appendChild(addElement);
|
||||||
|
return documentFragment;
|
||||||
|
}, document.createDocumentFragment());
|
||||||
};
|
};
|
||||||
|
|
||||||
const highlightElements = (elements = [], searchTerm) => {
|
const highlightText = (textNodes = [], searchTerm) => {
|
||||||
elements.forEach((element) => {
|
textNodes.forEach((textNode) => {
|
||||||
const replaceFragment = transformMatchElement(element, searchTerm);
|
const fragmentWithHighlights = highlightTextNode(textNode, searchTerm);
|
||||||
element.innerHTML = '';
|
textNode.parentElement.replaceChild(fragmentWithHighlights, textNode);
|
||||||
element.appendChild(replaceFragment);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const displayResults = ({ sectionSelector, expandSection, searchTerm }, matches) => {
|
const displayResults = ({ sectionSelector, expandSection, searchTerm }, matchingTextNodes) => {
|
||||||
const elements = matches.map((match) => match.parentElement);
|
const sections = Array.from(
|
||||||
const sections = uniq(elements.map((element) => findSettingsSection(sectionSelector, element)));
|
new Set(matchingTextNodes.map((node) => findSettingsSection(sectionSelector, node))),
|
||||||
|
);
|
||||||
|
|
||||||
hideSectionsExcept(sectionSelector, sections);
|
hideSectionsExcept(sectionSelector, sections);
|
||||||
sections.forEach(expandSection);
|
sections.forEach(expandSection);
|
||||||
highlightElements(elements, searchTerm);
|
highlightText(matchingTextNodes, searchTerm);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearResults = (params) => {
|
const clearResults = (params) => {
|
||||||
|
@ -114,13 +115,13 @@ const search = (root, searchTerm) => {
|
||||||
: NodeFilter.FILTER_REJECT;
|
: NodeFilter.FILTER_REJECT;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const results = [];
|
const textNodes = [];
|
||||||
|
|
||||||
for (let currentNode = iterator.nextNode(); currentNode; currentNode = iterator.nextNode()) {
|
for (let currentNode = iterator.nextNode(); currentNode; currentNode = iterator.nextNode()) {
|
||||||
results.push(currentNode);
|
textNodes.push(currentNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return textNodes;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
@import 'mixins_and_variables_and_functions';
|
@import 'mixins_and_variables_and_functions';
|
||||||
|
|
||||||
|
.import-jobs-from-col {
|
||||||
|
width: 37%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.import-jobs-to-col {
|
.import-jobs-to-col {
|
||||||
width: 39%;
|
width: 37%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.import-jobs-status-col {
|
.import-jobs-status-col {
|
||||||
width: 15%;
|
width: 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.import-jobs-cta-col {
|
.import-jobs-cta-col {
|
||||||
width: 1%;
|
width: 1%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.import-entities-target-select {
|
.import-entities-target-select {
|
||||||
&.disabled {
|
&.disabled {
|
||||||
.import-entities-target-select-separator {
|
.import-entities-target-select-separator {
|
||||||
|
|
|
@ -55,7 +55,8 @@ class UsersFinder
|
||||||
private
|
private
|
||||||
|
|
||||||
def base_scope
|
def base_scope
|
||||||
User.all.order_id_desc
|
scope = current_user&.admin? ? User.all : User.without_forbidden_states
|
||||||
|
scope.order_id_desc
|
||||||
end
|
end
|
||||||
|
|
||||||
def by_username(users)
|
def by_username(users)
|
||||||
|
|
|
@ -11,13 +11,10 @@ module Ci
|
||||||
self.limit_scope = :project
|
self.limit_scope = :project
|
||||||
self.limit_name = 'project_ci_secure_files'
|
self.limit_name = 'project_ci_secure_files'
|
||||||
|
|
||||||
attr_accessor :file_checksum
|
|
||||||
|
|
||||||
belongs_to :project, optional: false
|
belongs_to :project, optional: false
|
||||||
|
|
||||||
validates :file, presence: true, file_size: { maximum: FILE_SIZE_LIMIT }
|
validates :file, presence: true, file_size: { maximum: FILE_SIZE_LIMIT }
|
||||||
validates :checksum, :file_store, :name, :permissions, :project_id, presence: true
|
validates :checksum, :file_store, :name, :permissions, :project_id, presence: true
|
||||||
validate :validate_upload_checksum, on: :create
|
|
||||||
|
|
||||||
before_validation :assign_checksum
|
before_validation :assign_checksum
|
||||||
|
|
||||||
|
@ -36,11 +33,5 @@ module Ci
|
||||||
def assign_checksum
|
def assign_checksum
|
||||||
self.checksum = file.checksum if file.present? && file_changed?
|
self.checksum = file.checksum if file.present? && file_changed?
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_upload_checksum
|
|
||||||
unless self.file_checksum.nil?
|
|
||||||
errors.add(:file_checksum, _("Secure Files|File did not match the provided checksum")) unless self.file_checksum == self.checksum
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -46,6 +46,8 @@ class User < ApplicationRecord
|
||||||
:public_email
|
:public_email
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
|
FORBIDDEN_SEARCH_STATES = %w(blocked banned ldap_blocked).freeze
|
||||||
|
|
||||||
add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) }
|
add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) }
|
||||||
add_authentication_token_field :feed_token
|
add_authentication_token_field :feed_token
|
||||||
add_authentication_token_field :static_object_token, encrypted: :optional
|
add_authentication_token_field :static_object_token, encrypted: :optional
|
||||||
|
@ -469,6 +471,7 @@ class User < ApplicationRecord
|
||||||
scope :with_no_activity, -> { with_state(:active).human_or_service_user.where(last_activity_on: nil) }
|
scope :with_no_activity, -> { with_state(:active).human_or_service_user.where(last_activity_on: nil) }
|
||||||
scope :by_provider_and_extern_uid, ->(provider, extern_uid) { joins(:identities).merge(Identity.with_extern_uid(provider, extern_uid)) }
|
scope :by_provider_and_extern_uid, ->(provider, extern_uid) { joins(:identities).merge(Identity.with_extern_uid(provider, extern_uid)) }
|
||||||
scope :by_ids_or_usernames, -> (ids, usernames) { where(username: usernames).or(where(id: ids)) }
|
scope :by_ids_or_usernames, -> (ids, usernames) { where(username: usernames).or(where(id: ids)) }
|
||||||
|
scope :without_forbidden_states, -> { confirmed.where.not(state: FORBIDDEN_SEARCH_STATES) }
|
||||||
|
|
||||||
strip_attributes! :name
|
strip_attributes! :name
|
||||||
|
|
||||||
|
|
|
@ -5,4 +5,14 @@ class UserCustomAttribute < ApplicationRecord
|
||||||
|
|
||||||
validates :user_id, :key, :value, presence: true
|
validates :user_id, :key, :value, presence: true
|
||||||
validates :key, uniqueness: { scope: [:user_id] }
|
validates :key, uniqueness: { scope: [:user_id] }
|
||||||
|
|
||||||
|
def self.upsert_custom_attributes(custom_attributes)
|
||||||
|
created_at = Date.today
|
||||||
|
updated_at = Date.today
|
||||||
|
|
||||||
|
custom_attributes.map! do |custom_attribute|
|
||||||
|
custom_attribute.merge({ created_at: created_at, updated_at: updated_at })
|
||||||
|
end
|
||||||
|
upsert_all(custom_attributes, unique_by: [:user_id, :key])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddForbiddenStateIndexToUsers < Gitlab::Database::Migration[1.0]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
INDEX_NAME = 'users_forbidden_state_idx'
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_concurrent_index :users, :id,
|
||||||
|
name: INDEX_NAME,
|
||||||
|
where: "confirmed_at IS NOT NULL AND (state <> ALL (ARRAY['blocked', 'banned', 'ldap_blocked']))"
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_concurrent_index_by_name :users, INDEX_NAME
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1 @@
|
||||||
|
fcf7a6569afb7fdb95834179df5632ad14165d27476eb020e9db07e504f75f32
|
|
@ -29680,6 +29680,8 @@ CREATE UNIQUE INDEX unique_merge_request_metrics_by_merge_request_id ON merge_re
|
||||||
|
|
||||||
CREATE INDEX user_follow_users_followee_id_idx ON user_follow_users USING btree (followee_id);
|
CREATE INDEX user_follow_users_followee_id_idx ON user_follow_users USING btree (followee_id);
|
||||||
|
|
||||||
|
CREATE INDEX users_forbidden_state_idx ON users USING btree (id) WHERE ((confirmed_at IS NOT NULL) AND ((state)::text <> ALL (ARRAY['blocked'::text, 'banned'::text, 'ldap_blocked'::text])));
|
||||||
|
|
||||||
CREATE UNIQUE INDEX vulnerability_feedback_unique_idx ON vulnerability_feedback USING btree (project_id, category, feedback_type, project_fingerprint);
|
CREATE UNIQUE INDEX vulnerability_feedback_unique_idx ON vulnerability_feedback USING btree (project_id, category, feedback_type, project_fingerprint);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX vulnerability_occurrence_pipelines_on_unique_keys ON vulnerability_occurrence_pipelines USING btree (occurrence_id, pipeline_id);
|
CREATE UNIQUE INDEX vulnerability_occurrence_pipelines_on_unique_keys ON vulnerability_occurrence_pipelines USING btree (occurrence_id, pipeline_id);
|
||||||
|
|
|
@ -1533,6 +1533,26 @@ On each node:
|
||||||
# Recommended to be enabled for improved performance but can notably increase disk I/O
|
# Recommended to be enabled for improved performance but can notably increase disk I/O
|
||||||
# Refer to https://docs.gitlab.com/ee/administration/gitaly/configure_gitaly.html#pack-objects-cache for more info
|
# Refer to https://docs.gitlab.com/ee/administration/gitaly/configure_gitaly.html#pack-objects-cache for more info
|
||||||
gitaly['pack_objects_cache_enabled'] = true
|
gitaly['pack_objects_cache_enabled'] = true
|
||||||
|
|
||||||
|
# Configure the Consul agent
|
||||||
|
consul['enable'] = true
|
||||||
|
## Enable service discovery for Prometheus
|
||||||
|
consul['monitoring_service_discovery'] = true
|
||||||
|
|
||||||
|
# START user configuration
|
||||||
|
# Please set the real values as explained in Required Information section
|
||||||
|
#
|
||||||
|
## The IPs of the Consul server nodes
|
||||||
|
## You can also use FQDNs and intermix them with IPs
|
||||||
|
consul['configuration'] = {
|
||||||
|
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set the network addresses that the exporters will listen on for monitoring
|
||||||
|
node_exporter['listen_address'] = '0.0.0.0:9100'
|
||||||
|
gitaly['prometheus_listen_addr'] = '0.0.0.0:9236'
|
||||||
|
#
|
||||||
|
# END user configuration
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server:
|
1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server:
|
||||||
|
@ -2095,6 +2115,30 @@ To configure the Monitoring node:
|
||||||
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13)
|
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Configure Prometheus to scrape services not covered by discovery
|
||||||
|
prometheus['scrape_configs'] = [
|
||||||
|
{
|
||||||
|
'job_name': 'pgbouncer',
|
||||||
|
'static_configs' => [
|
||||||
|
'targets' => [
|
||||||
|
"10.6.0.31:9188",
|
||||||
|
"10.6.0.32:9188",
|
||||||
|
"10.6.0.33:9188",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'job_name': 'praefect',
|
||||||
|
'static_configs' => [
|
||||||
|
'targets' => [
|
||||||
|
"10.6.0.131:9652",
|
||||||
|
"10.6.0.132:9652",
|
||||||
|
"10.6.0.133:9652",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
# Nginx - For Grafana access
|
# Nginx - For Grafana access
|
||||||
nginx['enable'] = true
|
nginx['enable'] = true
|
||||||
```
|
```
|
||||||
|
|
|
@ -1537,6 +1537,26 @@ On each node:
|
||||||
# Recommended to be enabled for improved performance but can notably increase disk I/O
|
# Recommended to be enabled for improved performance but can notably increase disk I/O
|
||||||
# Refer to https://docs.gitlab.com/ee/administration/gitaly/configure_gitaly.html#pack-objects-cache for more info
|
# Refer to https://docs.gitlab.com/ee/administration/gitaly/configure_gitaly.html#pack-objects-cache for more info
|
||||||
gitaly['pack_objects_cache_enabled'] = true
|
gitaly['pack_objects_cache_enabled'] = true
|
||||||
|
|
||||||
|
# Configure the Consul agent
|
||||||
|
consul['enable'] = true
|
||||||
|
## Enable service discovery for Prometheus
|
||||||
|
consul['monitoring_service_discovery'] = true
|
||||||
|
|
||||||
|
# START user configuration
|
||||||
|
# Please set the real values as explained in Required Information section
|
||||||
|
#
|
||||||
|
## The IPs of the Consul server nodes
|
||||||
|
## You can also use FQDNs and intermix them with IPs
|
||||||
|
consul['configuration'] = {
|
||||||
|
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set the network addresses that the exporters will listen on for monitoring
|
||||||
|
node_exporter['listen_address'] = '0.0.0.0:9100'
|
||||||
|
gitaly['prometheus_listen_addr'] = '0.0.0.0:9236'
|
||||||
|
#
|
||||||
|
# END user configuration
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server:
|
1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server:
|
||||||
|
@ -2100,6 +2120,30 @@ To configure the Monitoring node:
|
||||||
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13)
|
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Configure Prometheus to scrape services not covered by discovery
|
||||||
|
prometheus['scrape_configs'] = [
|
||||||
|
{
|
||||||
|
'job_name': 'pgbouncer',
|
||||||
|
'static_configs' => [
|
||||||
|
'targets' => [
|
||||||
|
"10.6.0.31:9188",
|
||||||
|
"10.6.0.32:9188",
|
||||||
|
"10.6.0.33:9188",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'job_name': 'praefect',
|
||||||
|
'static_configs' => [
|
||||||
|
'targets' => [
|
||||||
|
"10.6.0.131:9652",
|
||||||
|
"10.6.0.132:9652",
|
||||||
|
"10.6.0.133:9652",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
# Nginx - For Grafana access
|
# Nginx - For Grafana access
|
||||||
nginx['enable'] = true
|
nginx['enable'] = true
|
||||||
```
|
```
|
||||||
|
|
|
@ -1477,6 +1477,26 @@ On each node:
|
||||||
# Recommended to be enabled for improved performance but can notably increase disk I/O
|
# Recommended to be enabled for improved performance but can notably increase disk I/O
|
||||||
# Refer to https://docs.gitlab.com/ee/administration/gitaly/configure_gitaly.html#pack-objects-cache for more info
|
# Refer to https://docs.gitlab.com/ee/administration/gitaly/configure_gitaly.html#pack-objects-cache for more info
|
||||||
gitaly['pack_objects_cache_enabled'] = true
|
gitaly['pack_objects_cache_enabled'] = true
|
||||||
|
|
||||||
|
# Configure the Consul agent
|
||||||
|
consul['enable'] = true
|
||||||
|
## Enable service discovery for Prometheus
|
||||||
|
consul['monitoring_service_discovery'] = true
|
||||||
|
|
||||||
|
# START user configuration
|
||||||
|
# Please set the real values as explained in Required Information section
|
||||||
|
#
|
||||||
|
## The IPs of the Consul server nodes
|
||||||
|
## You can also use FQDNs and intermix them with IPs
|
||||||
|
consul['configuration'] = {
|
||||||
|
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set the network addresses that the exporters will listen on for monitoring
|
||||||
|
node_exporter['listen_address'] = '0.0.0.0:9100'
|
||||||
|
gitaly['prometheus_listen_addr'] = '0.0.0.0:9236'
|
||||||
|
#
|
||||||
|
# END user configuration
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server:
|
1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server:
|
||||||
|
@ -2020,6 +2040,30 @@ running [Prometheus](../monitoring/prometheus/index.md) and
|
||||||
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13)
|
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Configure Prometheus to scrape services not covered by discovery
|
||||||
|
prometheus['scrape_configs'] = [
|
||||||
|
{
|
||||||
|
'job_name': 'pgbouncer',
|
||||||
|
'static_configs' => [
|
||||||
|
'targets' => [
|
||||||
|
"10.6.0.21:9188",
|
||||||
|
"10.6.0.22:9188",
|
||||||
|
"10.6.0.23:9188",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'job_name': 'praefect',
|
||||||
|
'static_configs' => [
|
||||||
|
'targets' => [
|
||||||
|
"10.6.0.131:9652",
|
||||||
|
"10.6.0.132:9652",
|
||||||
|
"10.6.0.133:9652",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
# Nginx - For Grafana access
|
# Nginx - For Grafana access
|
||||||
nginx['enable'] = true
|
nginx['enable'] = true
|
||||||
```
|
```
|
||||||
|
|
|
@ -1546,6 +1546,26 @@ On each node:
|
||||||
# Recommended to be enabled for improved performance but can notably increase disk I/O
|
# Recommended to be enabled for improved performance but can notably increase disk I/O
|
||||||
# Refer to https://docs.gitlab.com/ee/administration/gitaly/configure_gitaly.html#pack-objects-cache for more info
|
# Refer to https://docs.gitlab.com/ee/administration/gitaly/configure_gitaly.html#pack-objects-cache for more info
|
||||||
gitaly['pack_objects_cache_enabled'] = true
|
gitaly['pack_objects_cache_enabled'] = true
|
||||||
|
|
||||||
|
# Configure the Consul agent
|
||||||
|
consul['enable'] = true
|
||||||
|
## Enable service discovery for Prometheus
|
||||||
|
consul['monitoring_service_discovery'] = true
|
||||||
|
|
||||||
|
# START user configuration
|
||||||
|
# Please set the real values as explained in Required Information section
|
||||||
|
#
|
||||||
|
## The IPs of the Consul server nodes
|
||||||
|
## You can also use FQDNs and intermix them with IPs
|
||||||
|
consul['configuration'] = {
|
||||||
|
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set the network addresses that the exporters will listen on for monitoring
|
||||||
|
node_exporter['listen_address'] = '0.0.0.0:9100'
|
||||||
|
gitaly['prometheus_listen_addr'] = '0.0.0.0:9236'
|
||||||
|
#
|
||||||
|
# END user configuration
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server:
|
1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server:
|
||||||
|
@ -2116,6 +2136,30 @@ To configure the Monitoring node:
|
||||||
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13)
|
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Configure Prometheus to scrape services not covered by discovery
|
||||||
|
prometheus['scrape_configs'] = [
|
||||||
|
{
|
||||||
|
'job_name': 'pgbouncer',
|
||||||
|
'static_configs' => [
|
||||||
|
'targets' => [
|
||||||
|
"10.6.0.31:9188",
|
||||||
|
"10.6.0.32:9188",
|
||||||
|
"10.6.0.33:9188",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'job_name': 'praefect',
|
||||||
|
'static_configs' => [
|
||||||
|
'targets' => [
|
||||||
|
"10.6.0.131:9652",
|
||||||
|
"10.6.0.132:9652",
|
||||||
|
"10.6.0.133:9652",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
# Nginx - For Grafana access
|
# Nginx - For Grafana access
|
||||||
nginx['enable'] = true
|
nginx['enable'] = true
|
||||||
```
|
```
|
||||||
|
|
|
@ -1475,6 +1475,26 @@ On each node:
|
||||||
# Recommended to be enabled for improved performance but can notably increase disk I/O
|
# Recommended to be enabled for improved performance but can notably increase disk I/O
|
||||||
# Refer to https://docs.gitlab.com/ee/administration/gitaly/configure_gitaly.html#pack-objects-cache for more info
|
# Refer to https://docs.gitlab.com/ee/administration/gitaly/configure_gitaly.html#pack-objects-cache for more info
|
||||||
gitaly['pack_objects_cache_enabled'] = true
|
gitaly['pack_objects_cache_enabled'] = true
|
||||||
|
|
||||||
|
# Configure the Consul agent
|
||||||
|
consul['enable'] = true
|
||||||
|
## Enable service discovery for Prometheus
|
||||||
|
consul['monitoring_service_discovery'] = true
|
||||||
|
|
||||||
|
# START user configuration
|
||||||
|
# Please set the real values as explained in Required Information section
|
||||||
|
#
|
||||||
|
## The IPs of the Consul server nodes
|
||||||
|
## You can also use FQDNs and intermix them with IPs
|
||||||
|
consul['configuration'] = {
|
||||||
|
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set the network addresses that the exporters will listen on for monitoring
|
||||||
|
node_exporter['listen_address'] = '0.0.0.0:9100'
|
||||||
|
gitaly['prometheus_listen_addr'] = '0.0.0.0:9236'
|
||||||
|
#
|
||||||
|
# END user configuration
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server:
|
1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server:
|
||||||
|
@ -2020,6 +2040,30 @@ running [Prometheus](../monitoring/prometheus/index.md) and
|
||||||
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13)
|
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Configure Prometheus to scrape services not covered by discovery
|
||||||
|
prometheus['scrape_configs'] = [
|
||||||
|
{
|
||||||
|
'job_name': 'pgbouncer',
|
||||||
|
'static_configs' => [
|
||||||
|
'targets' => [
|
||||||
|
"10.6.0.31:9188",
|
||||||
|
"10.6.0.32:9188",
|
||||||
|
"10.6.0.33:9188",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'job_name': 'praefect',
|
||||||
|
'static_configs' => [
|
||||||
|
'targets' => [
|
||||||
|
"10.6.0.131:9652",
|
||||||
|
"10.6.0.132:9652",
|
||||||
|
"10.6.0.133:9652",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
# Nginx - For Grafana access
|
# Nginx - For Grafana access
|
||||||
nginx['enable'] = true
|
nginx['enable'] = true
|
||||||
```
|
```
|
||||||
|
|
|
@ -257,6 +257,13 @@ ProjectDestroyWorker.perform_async(project.id, user.id, {})
|
||||||
# or Projects::DestroyService.new(project, user).execute
|
# or Projects::DestroyService.new(project, user).execute
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If this fails, display why it doesn't work with:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
project = Project.find_by_full_path('<project_path>')
|
||||||
|
project.delete_error
|
||||||
|
```
|
||||||
|
|
||||||
### Remove fork relationship manually
|
### Remove fork relationship manually
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
|
|
|
@ -105,7 +105,6 @@ Supported attributes:
|
||||||
| `project_id` | integer/string | **{check-circle}** Yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. |
|
| `project_id` | integer/string | **{check-circle}** Yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. |
|
||||||
| `name` | string | **{check-circle}** Yes | The `name` of the file being uploaded. |
|
| `name` | string | **{check-circle}** Yes | The `name` of the file being uploaded. |
|
||||||
| `file` | file | **{check-circle}** Yes | The `file` being uploaded (5 MB limit). |
|
| `file` | file | **{check-circle}** Yes | The `file` being uploaded (5 MB limit). |
|
||||||
| `file_checksum` | file | **{dotted-circle}** No | An optional sha256 checksum of the file to be uploaded. If provided, the checksum must match the uploaded file, or the upload will fail to validate. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/355653) in GitLab 14.10. |
|
|
||||||
| `permissions` | string | **{dotted-circle}** No | The file is created with the specified permissions when created in the CI/CD job. Available types are: `read_only` (default), `read_write`, and `execute`. |
|
| `permissions` | string | **{dotted-circle}** No | The file is created with the specified permissions when created in the CI/CD job. Available types are: `read_only` (default), `read_write`, and `execute`. |
|
||||||
|
|
||||||
Example request:
|
Example request:
|
||||||
|
|
|
@ -232,3 +232,38 @@ too small, the error persists.
|
||||||
|
|
||||||
Modifying the server is not always an option, and introduces more potential risk.
|
Modifying the server is not always an option, and introduces more potential risk.
|
||||||
Attempt local changes first.
|
Attempt local changes first.
|
||||||
|
|
||||||
|
## Password expired error on Git fetch via SSH for LDAP user
|
||||||
|
|
||||||
|
If `git fetch` returns this `HTTP 403 Forbidden` error on a self-managed instance of
|
||||||
|
GitLab, the password expiration date (`users.password_expires_at`) for this user in the
|
||||||
|
GitLab database is a date in the past:
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
Your password expired. Please access GitLab from a web browser to update your password.
|
||||||
|
```
|
||||||
|
|
||||||
|
Requests made with a SSO account and where `password_expires_at` is not `null`
|
||||||
|
return this error:
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
"403 Forbidden - Your password expired. Please access GitLab from a web browser to update your password."
|
||||||
|
```
|
||||||
|
|
||||||
|
To resolve this issue, you can update the password expiration by either:
|
||||||
|
|
||||||
|
- Using the `gitlab-rails console`:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
gitlab-rails console
|
||||||
|
user.update!(password_expires_at: nil)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Using `gitlab-psql`:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
# gitlab-psql
|
||||||
|
UPDATE users SET password_expires_at = null WHERE username='<USERNAME>';
|
||||||
|
```
|
||||||
|
|
||||||
|
The bug was reported [in this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/332455).
|
||||||
|
|
|
@ -10,6 +10,11 @@ disqus_identifier: 'https://docs.gitlab.com/ee/workflow/repository_mirroring.htm
|
||||||
You can _mirror_ a repository to and from external sources. You can select which repository
|
You can _mirror_ a repository to and from external sources. You can select which repository
|
||||||
serves as the source. Branches, tags, and commits can be mirrored.
|
serves as the source. Branches, tags, and commits can be mirrored.
|
||||||
|
|
||||||
|
NOTE:
|
||||||
|
SCP-style URLs are **not** supported. However, the work for implementing SCP-style URLs is tracked
|
||||||
|
in [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/18993).
|
||||||
|
Subscribe to the issue to follow its progress.
|
||||||
|
|
||||||
Several mirroring methods exist:
|
Several mirroring methods exist:
|
||||||
|
|
||||||
- [Push](push.md): for mirroring a GitLab repository to another location.
|
- [Push](push.md): for mirroring a GitLab repository to another location.
|
||||||
|
|
|
@ -62,14 +62,12 @@ module API
|
||||||
requires :name, type: String, desc: 'The name of the file'
|
requires :name, type: String, desc: 'The name of the file'
|
||||||
requires :file, types: [Rack::Multipart::UploadedFile, ::API::Validations::Types::WorkhorseFile], desc: 'The secure file to be uploaded'
|
requires :file, types: [Rack::Multipart::UploadedFile, ::API::Validations::Types::WorkhorseFile], desc: 'The secure file to be uploaded'
|
||||||
optional :permissions, type: String, desc: 'The file permissions', default: 'read_only', values: %w[read_only read_write execute]
|
optional :permissions, type: String, desc: 'The file permissions', default: 'read_only', values: %w[read_only read_write execute]
|
||||||
optional :file_checksum, type: String, desc: 'An optional sha256 checksum of the file to be uploaded'
|
|
||||||
end
|
end
|
||||||
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: true
|
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: true
|
||||||
post ':id/secure_files' do
|
post ':id/secure_files' do
|
||||||
secure_file = user_project.secure_files.new(
|
secure_file = user_project.secure_files.new(
|
||||||
name: params[:name],
|
name: params[:name],
|
||||||
permissions: params[:permissions] || :read_only,
|
permissions: params[:permissions] || :read_only
|
||||||
file_checksum: params[:file_checksum]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
secure_file.file = params[:file]
|
secure_file.file = params[:file]
|
||||||
|
|
|
@ -13016,6 +13016,9 @@ msgstr ""
|
||||||
msgid "Diff limits"
|
msgid "Diff limits"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Diff notes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Difference between start date and now"
|
msgid "Difference between start date and now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -17085,6 +17088,15 @@ msgstr ""
|
||||||
msgid "Gitea Import"
|
msgid "Gitea Import"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "GithubImporter|PR mergers"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "GithubImporter|PR reviews"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "GithubImporter|Pull requests"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "GithubIntegration|Create a %{token_link_start}personal access token%{token_link_end} with %{status_html} access granted and paste it here."
|
msgid "GithubIntegration|Create a %{token_link_start}personal access token%{token_link_end} with %{status_html} access granted and paste it here."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -25642,6 +25654,9 @@ msgstr ""
|
||||||
msgid "NoteForm|Note"
|
msgid "NoteForm|Note"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Notes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Notes rate limit"
|
msgid "Notes rate limit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -27010,6 +27025,9 @@ msgstr ""
|
||||||
msgid "Part of merge request changes"
|
msgid "Part of merge request changes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Partial import"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Participants"
|
msgid "Participants"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -33128,9 +33146,6 @@ msgstr ""
|
||||||
msgid "Secure Files"
|
msgid "Secure Files"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Secure Files|File did not match the provided checksum"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Secure token that identifies an external storage request."
|
msgid "Secure token that identifies an external storage request."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ describe QA::Runtime::AllureReport do
|
||||||
described_class.configure!
|
described_class.configure!
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'configures Allure options' do
|
it 'configures Allure options', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/357816' do
|
||||||
aggregate_failures do
|
aggregate_failures do
|
||||||
expect(allure_config.results_directory).to eq('tmp/allure-results')
|
expect(allure_config.results_directory).to eq('tmp/allure-results')
|
||||||
expect(allure_config.clean_results_directory).to eq(true)
|
expect(allure_config.clean_results_directory).to eq(true)
|
||||||
|
|
|
@ -11,10 +11,10 @@ RSpec.describe UsersFinder do
|
||||||
context 'with a normal user' do
|
context 'with a normal user' do
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
it 'returns all users' do
|
it 'returns searchable users' do
|
||||||
users = described_class.new(user).execute
|
users = described_class.new(user).execute
|
||||||
|
|
||||||
expect(users).to contain_exactly(user, normal_user, blocked_user, external_user, omniauth_user, internal_user, admin_user, project_bot)
|
expect(users).to contain_exactly(user, normal_user, external_user, omniauth_user, internal_user, admin_user, project_bot)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'filters by username' do
|
it 'filters by username' do
|
||||||
|
@ -36,9 +36,9 @@ RSpec.describe UsersFinder do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'filters by search' do
|
it 'filters by search' do
|
||||||
users = described_class.new(user, search: 'orando').execute
|
users = described_class.new(user, search: 'ohndo').execute
|
||||||
|
|
||||||
expect(users).to contain_exactly(blocked_user)
|
expect(users).to contain_exactly(normal_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not filter by private emails search' do
|
it 'does not filter by private emails search' do
|
||||||
|
@ -47,18 +47,6 @@ RSpec.describe UsersFinder do
|
||||||
expect(users).to be_empty
|
expect(users).to be_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'filters by blocked users' do
|
|
||||||
users = described_class.new(user, blocked: true).execute
|
|
||||||
|
|
||||||
expect(users).to contain_exactly(blocked_user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'filters by active users' do
|
|
||||||
users = described_class.new(user, active: true).execute
|
|
||||||
|
|
||||||
expect(users).to contain_exactly(user, normal_user, external_user, omniauth_user, admin_user, project_bot)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'filters by external users' do
|
it 'filters by external users' do
|
||||||
users = described_class.new(user, external: true).execute
|
users = described_class.new(user, external: true).execute
|
||||||
|
|
||||||
|
@ -68,7 +56,7 @@ RSpec.describe UsersFinder do
|
||||||
it 'filters by non external users' do
|
it 'filters by non external users' do
|
||||||
users = described_class.new(user, non_external: true).execute
|
users = described_class.new(user, non_external: true).execute
|
||||||
|
|
||||||
expect(users).to contain_exactly(user, normal_user, blocked_user, omniauth_user, internal_user, admin_user, project_bot)
|
expect(users).to contain_exactly(user, normal_user, omniauth_user, internal_user, admin_user, project_bot)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'filters by created_at' do
|
it 'filters by created_at' do
|
||||||
|
@ -85,13 +73,7 @@ RSpec.describe UsersFinder do
|
||||||
it 'filters by non internal users' do
|
it 'filters by non internal users' do
|
||||||
users = described_class.new(user, non_internal: true).execute
|
users = described_class.new(user, non_internal: true).execute
|
||||||
|
|
||||||
expect(users).to contain_exactly(user, normal_user, external_user, blocked_user, omniauth_user, admin_user, project_bot)
|
expect(users).to contain_exactly(user, normal_user, external_user, omniauth_user, admin_user, project_bot)
|
||||||
end
|
|
||||||
|
|
||||||
it 'filters by without project bots' do
|
|
||||||
users = described_class.new(user, without_project_bots: true).execute
|
|
||||||
|
|
||||||
expect(users).to contain_exactly(user, normal_user, external_user, blocked_user, omniauth_user, internal_user, admin_user)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not filter by custom attributes' do
|
it 'does not filter by custom attributes' do
|
||||||
|
@ -100,18 +82,18 @@ RSpec.describe UsersFinder do
|
||||||
custom_attributes: { foo: 'bar' }
|
custom_attributes: { foo: 'bar' }
|
||||||
).execute
|
).execute
|
||||||
|
|
||||||
expect(users).to contain_exactly(user, normal_user, blocked_user, external_user, omniauth_user, internal_user, admin_user, project_bot)
|
expect(users).to contain_exactly(user, normal_user, external_user, omniauth_user, internal_user, admin_user, project_bot)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'orders returned results' do
|
it 'orders returned results' do
|
||||||
users = described_class.new(user, sort: 'id_asc').execute
|
users = described_class.new(user, sort: 'id_asc').execute
|
||||||
|
|
||||||
expect(users).to eq([normal_user, admin_user, blocked_user, external_user, omniauth_user, internal_user, project_bot, user])
|
expect(users).to eq([normal_user, admin_user, external_user, omniauth_user, internal_user, project_bot, user])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not filter by admins' do
|
it 'does not filter by admins' do
|
||||||
users = described_class.new(user, admins: true).execute
|
users = described_class.new(user, admins: true).execute
|
||||||
expect(users).to contain_exactly(user, normal_user, external_user, admin_user, blocked_user, omniauth_user, internal_user, project_bot)
|
expect(users).to contain_exactly(user, normal_user, external_user, admin_user, omniauth_user, internal_user, project_bot)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -127,7 +109,19 @@ RSpec.describe UsersFinder do
|
||||||
it 'returns all users' do
|
it 'returns all users' do
|
||||||
users = described_class.new(admin).execute
|
users = described_class.new(admin).execute
|
||||||
|
|
||||||
expect(users).to contain_exactly(admin, normal_user, blocked_user, external_user, omniauth_user, internal_user, admin_user, project_bot)
|
expect(users).to contain_exactly(admin, normal_user, blocked_user, unconfirmed_user, banned_user, external_user, omniauth_user, internal_user, admin_user, project_bot)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filters by blocked users' do
|
||||||
|
users = described_class.new(admin, blocked: true).execute
|
||||||
|
|
||||||
|
expect(users).to contain_exactly(blocked_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filters by active users' do
|
||||||
|
users = described_class.new(admin, active: true).execute
|
||||||
|
|
||||||
|
expect(users).to contain_exactly(admin, normal_user, unconfirmed_user, external_user, omniauth_user, admin_user, project_bot)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns only admins' do
|
it 'returns only admins' do
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
import { GlAccordionItem, GlBadge, GlIcon } from '@gitlab/ui';
|
||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import ImportStatus from '~/import_entities/components/import_status.vue';
|
||||||
|
import { STATUSES } from '~/import_entities/constants';
|
||||||
|
|
||||||
|
describe('Import entities status component', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
const createComponent = (propsData) => {
|
||||||
|
wrapper = shallowMount(ImportStatus, {
|
||||||
|
propsData,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('success status', () => {
|
||||||
|
const getStatusText = () => wrapper.findComponent(GlBadge).text();
|
||||||
|
|
||||||
|
it('displays finished status as complete when no stats are provided', () => {
|
||||||
|
createComponent({
|
||||||
|
status: STATUSES.FINISHED,
|
||||||
|
});
|
||||||
|
expect(getStatusText()).toBe('Complete');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays finished status as complete when all stats items were processed', () => {
|
||||||
|
const statItems = { label: 100, note: 200 };
|
||||||
|
|
||||||
|
createComponent({
|
||||||
|
status: STATUSES.FINISHED,
|
||||||
|
stats: {
|
||||||
|
fetched: { ...statItems },
|
||||||
|
imported: { ...statItems },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getStatusText()).toBe('Complete');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays finished status as partial when all stats items were processed', () => {
|
||||||
|
const statItems = { label: 100, note: 200 };
|
||||||
|
|
||||||
|
createComponent({
|
||||||
|
status: STATUSES.FINISHED,
|
||||||
|
stats: {
|
||||||
|
fetched: { ...statItems },
|
||||||
|
imported: { ...statItems, label: 50 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getStatusText()).toBe('Partial import');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('details drawer', () => {
|
||||||
|
const findDetailsDrawer = () => wrapper.findComponent(GlAccordionItem);
|
||||||
|
|
||||||
|
it('renders details drawer to be present when stats are provided', () => {
|
||||||
|
createComponent({
|
||||||
|
status: 'created',
|
||||||
|
stats: { fetched: { label: 1 }, imported: { label: 0 } },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findDetailsDrawer().exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render details drawer when no stats are provided', () => {
|
||||||
|
createComponent({
|
||||||
|
status: 'created',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findDetailsDrawer().exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render details drawer when stats are empty', () => {
|
||||||
|
createComponent({
|
||||||
|
status: 'created',
|
||||||
|
stats: { fetched: {}, imported: {} },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findDetailsDrawer().exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render details drawer when no known stats are provided', () => {
|
||||||
|
createComponent({
|
||||||
|
status: 'created',
|
||||||
|
stats: {
|
||||||
|
fetched: {
|
||||||
|
UNKNOWN_STAT: 100,
|
||||||
|
},
|
||||||
|
imported: {
|
||||||
|
UNKNOWN_STAT: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findDetailsDrawer().exists()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stats display', () => {
|
||||||
|
const getStatusIcon = () =>
|
||||||
|
wrapper.findComponent(GlAccordionItem).findComponent(GlIcon).props().name;
|
||||||
|
|
||||||
|
const createComponentWithStats = ({ fetched, imported }) => {
|
||||||
|
createComponent({
|
||||||
|
status: 'created',
|
||||||
|
stats: {
|
||||||
|
fetched: { label: fetched },
|
||||||
|
imported: { label: imported },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
it('displays scheduled status when imported is 0', () => {
|
||||||
|
createComponentWithStats({
|
||||||
|
fetched: 100,
|
||||||
|
imported: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getStatusIcon()).toBe('status-scheduled');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays running status when imported is not equal to fetched', () => {
|
||||||
|
createComponentWithStats({
|
||||||
|
fetched: 100,
|
||||||
|
imported: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getStatusIcon()).toBe('status-running');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays success status when imported is equal to fetched', () => {
|
||||||
|
createComponentWithStats({
|
||||||
|
fetched: 100,
|
||||||
|
imported: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getStatusIcon()).toBe('status-success');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -98,6 +98,8 @@ describe('ProviderRepoTableRow', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when rendering imported project', () => {
|
describe('when rendering imported project', () => {
|
||||||
|
const FAKE_STATS = {};
|
||||||
|
|
||||||
const repo = {
|
const repo = {
|
||||||
importSource: {
|
importSource: {
|
||||||
id: 'remote-1',
|
id: 'remote-1',
|
||||||
|
@ -109,6 +111,7 @@ describe('ProviderRepoTableRow', () => {
|
||||||
fullPath: 'fullPath',
|
fullPath: 'fullPath',
|
||||||
importSource: 'importSource',
|
importSource: 'importSource',
|
||||||
importStatus: STATUSES.FINISHED,
|
importStatus: STATUSES.FINISHED,
|
||||||
|
stats: FAKE_STATS,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -134,6 +137,10 @@ describe('ProviderRepoTableRow', () => {
|
||||||
it('does not render import button', () => {
|
it('does not render import button', () => {
|
||||||
expect(findImportButton().exists()).toBe(false);
|
expect(findImportButton().exists()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('passes stats to import status component', () => {
|
||||||
|
expect(wrapper.find(ImportStatus).props().stats).toBe(FAKE_STATS);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when rendering incompatible project', () => {
|
describe('when rendering incompatible project', () => {
|
||||||
|
|
|
@ -232,6 +232,35 @@ describe('import_projects store mutations', () => {
|
||||||
updatedProjects[0].importStatus,
|
updatedProjects[0].importStatus,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('updates import stats of project', () => {
|
||||||
|
const repoId = 1;
|
||||||
|
state = {
|
||||||
|
repositories: [
|
||||||
|
{ importedProject: { id: repoId, stats: {} }, importStatus: STATUSES.STARTED },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const newStats = {
|
||||||
|
fetched: {
|
||||||
|
label: 10,
|
||||||
|
},
|
||||||
|
imported: {
|
||||||
|
label: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedProjects = [
|
||||||
|
{
|
||||||
|
id: repoId,
|
||||||
|
importStatus: STATUSES.FINISHED,
|
||||||
|
stats: newStats,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
mutations[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects);
|
||||||
|
|
||||||
|
expect(state.repositories[0].importedProject.stats).toStrictEqual(newStats);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`${types.REQUEST_NAMESPACES}`, () => {
|
describe(`${types.REQUEST_NAMESPACES}`, () => {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { GlSearchBoxByType } from '@gitlab/ui';
|
import { GlSearchBoxByType } from '@gitlab/ui';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { setHTMLFixture } from 'helpers/fixtures';
|
||||||
import SearchSettings from '~/search_settings/components/search_settings.vue';
|
import SearchSettings from '~/search_settings/components/search_settings.vue';
|
||||||
import { HIGHLIGHT_CLASS, HIDE_CLASS } from '~/search_settings/constants';
|
import { HIGHLIGHT_CLASS, HIDE_CLASS } from '~/search_settings/constants';
|
||||||
import { isExpanded, expandSection, closeSection } from '~/settings_panels';
|
import { isExpanded, expandSection, closeSection } from '~/settings_panels';
|
||||||
|
@ -11,7 +12,8 @@ describe('search_settings/components/search_settings.vue', () => {
|
||||||
const GENERAL_SETTINGS_ID = 'js-general-settings';
|
const GENERAL_SETTINGS_ID = 'js-general-settings';
|
||||||
const ADVANCED_SETTINGS_ID = 'js-advanced-settings';
|
const ADVANCED_SETTINGS_ID = 'js-advanced-settings';
|
||||||
const EXTRA_SETTINGS_ID = 'js-extra-settings';
|
const EXTRA_SETTINGS_ID = 'js-extra-settings';
|
||||||
const TEXT_CONTAIN_SEARCH_TERM = `This text contain ${SEARCH_TERM} and <script>alert("111")</script> others.`;
|
const TEXT_CONTAIN_SEARCH_TERM = `This text contain ${SEARCH_TERM}.`;
|
||||||
|
const TEXT_WITH_SIBLING_ELEMENTS = `${SEARCH_TERM} <a data-testid="sibling" href="#">Learn more</a>.`;
|
||||||
|
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
|
@ -42,13 +44,7 @@ describe('search_settings/components/search_settings.vue', () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const matchParentElement = () => {
|
const findMatchSiblingElement = () => document.querySelector(`[data-testid="sibling"]`);
|
||||||
const highlightedList = Array.from(document.querySelectorAll(`.${HIGHLIGHT_CLASS}`));
|
|
||||||
return highlightedList.map((element) => {
|
|
||||||
return element.parentNode;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const findSearchBox = () => wrapper.find(GlSearchBoxByType);
|
const findSearchBox = () => wrapper.find(GlSearchBoxByType);
|
||||||
const search = (term) => {
|
const search = (term) => {
|
||||||
findSearchBox().vm.$emit('input', term);
|
findSearchBox().vm.$emit('input', term);
|
||||||
|
@ -56,7 +52,7 @@ describe('search_settings/components/search_settings.vue', () => {
|
||||||
const clearSearch = () => search('');
|
const clearSearch = () => search('');
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setFixtures(`
|
setHTMLFixture(`
|
||||||
<div>
|
<div>
|
||||||
<div class="js-search-app"></div>
|
<div class="js-search-app"></div>
|
||||||
<div id="${ROOT_ID}">
|
<div id="${ROOT_ID}">
|
||||||
|
@ -69,6 +65,7 @@ describe('search_settings/components/search_settings.vue', () => {
|
||||||
<section id="${EXTRA_SETTINGS_ID}" class="settings">
|
<section id="${EXTRA_SETTINGS_ID}" class="settings">
|
||||||
<span>${SEARCH_TERM}</span>
|
<span>${SEARCH_TERM}</span>
|
||||||
<span>${TEXT_CONTAIN_SEARCH_TERM}</span>
|
<span>${TEXT_CONTAIN_SEARCH_TERM}</span>
|
||||||
|
<span>${TEXT_WITH_SIBLING_ELEMENTS}</span>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -99,7 +96,7 @@ describe('search_settings/components/search_settings.vue', () => {
|
||||||
it('highlight elements that match the search term', () => {
|
it('highlight elements that match the search term', () => {
|
||||||
search(SEARCH_TERM);
|
search(SEARCH_TERM);
|
||||||
|
|
||||||
expect(highlightedElementsCount()).toBe(2);
|
expect(highlightedElementsCount()).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('highlight only search term and not the whole line', () => {
|
it('highlight only search term and not the whole line', () => {
|
||||||
|
@ -108,14 +105,26 @@ describe('search_settings/components/search_settings.vue', () => {
|
||||||
expect(highlightedTextNodes()).toBe(true);
|
expect(highlightedTextNodes()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('prevents search xss', () => {
|
// Regression test for https://gitlab.com/gitlab-org/gitlab/-/issues/350494
|
||||||
|
it('preserves elements that are siblings of matches', () => {
|
||||||
|
const snapshot = `
|
||||||
|
<a
|
||||||
|
data-testid="sibling"
|
||||||
|
href="#"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
|
||||||
|
expect(findMatchSiblingElement()).toMatchInlineSnapshot(snapshot);
|
||||||
|
|
||||||
search(SEARCH_TERM);
|
search(SEARCH_TERM);
|
||||||
|
|
||||||
const parentNodeList = matchParentElement();
|
expect(findMatchSiblingElement()).toMatchInlineSnapshot(snapshot);
|
||||||
parentNodeList.forEach((element) => {
|
|
||||||
const scriptElement = element.getElementsByTagName('script');
|
clearSearch();
|
||||||
expect(scriptElement.length).toBe(0);
|
|
||||||
});
|
expect(findMatchSiblingElement()).toMatchInlineSnapshot(snapshot);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('default', () => {
|
describe('default', () => {
|
||||||
|
|
|
@ -6642,6 +6642,23 @@ RSpec.describe User do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.without_forbidden_states' do
|
||||||
|
let_it_be(:normal_user) { create(:user, username: 'johndoe') }
|
||||||
|
let_it_be(:admin_user) { create(:user, :admin, username: 'iamadmin') }
|
||||||
|
let_it_be(:blocked_user) { create(:user, :blocked, username: 'notsorandom') }
|
||||||
|
let_it_be(:banned_user) { create(:user, :banned, username: 'iambanned') }
|
||||||
|
let_it_be(:external_user) { create(:user, :external) }
|
||||||
|
let_it_be(:unconfirmed_user) { create(:user, confirmed_at: nil) }
|
||||||
|
let_it_be(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
|
||||||
|
let_it_be(:internal_user) { User.alert_bot.tap { |u| u.confirm } }
|
||||||
|
|
||||||
|
it 'does not return blocked, banned or unconfirmed users' do
|
||||||
|
expect(described_class.without_forbidden_states).to match_array([
|
||||||
|
normal_user, admin_user, external_user, omniauth_user, internal_user
|
||||||
|
])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'user_project' do
|
describe 'user_project' do
|
||||||
it 'returns users project matched by username and public visibility' do
|
it 'returns users project matched by username and public visibility' do
|
||||||
user = create(:user)
|
user = create(:user)
|
||||||
|
|
|
@ -257,22 +257,6 @@ RSpec.describe API::Ci::SecureFiles do
|
||||||
expect(Base64.encode64(response.body)).to eq(Base64.encode64(fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks').read))
|
expect(Base64.encode64(response.body)).to eq(Base64.encode64(fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks').read))
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'uploads and validates a secure file with a provided checksum' do
|
|
||||||
params = {
|
|
||||||
file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks'),
|
|
||||||
name: 'upload-keystore.jks',
|
|
||||||
permissions: 'execute',
|
|
||||||
file_checksum: Digest::SHA256.hexdigest(File.read(fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks')))
|
|
||||||
}
|
|
||||||
|
|
||||||
expect do
|
|
||||||
post api("/projects/#{project.id}/secure_files", maintainer), params: params
|
|
||||||
end.to change {project.secure_files.count}.by(1)
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:created)
|
|
||||||
expect(json_response['name']).to eq('upload-keystore.jks')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns an error when the file checksum fails to validate' do
|
it 'returns an error when the file checksum fails to validate' do
|
||||||
secure_file.update!(checksum: 'foo')
|
secure_file.update!(checksum: 'foo')
|
||||||
|
|
||||||
|
@ -283,22 +267,6 @@ RSpec.describe API::Ci::SecureFiles do
|
||||||
expect(response.code).to eq("500")
|
expect(response.code).to eq("500")
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns an error when the user provided file checksum fails to validate' do
|
|
||||||
post_params = {
|
|
||||||
file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks'),
|
|
||||||
name: 'upload-keystore.jks',
|
|
||||||
permissions: 'read_write',
|
|
||||||
file_checksum: 'foo'
|
|
||||||
}
|
|
||||||
|
|
||||||
expect do
|
|
||||||
post api("/projects/#{project.id}/secure_files", maintainer), params: post_params
|
|
||||||
end.not_to change { project.secure_files.count }
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
|
||||||
expect(json_response['message']['file_checksum']).to include(_("Secure Files|File did not match the provided checksum"))
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns an error when no file is uploaded' do
|
it 'returns an error when no file is uploaded' do
|
||||||
post_params = {
|
post_params = {
|
||||||
name: 'upload-keystore.jks'
|
name: 'upload-keystore.jks'
|
||||||
|
|
|
@ -338,12 +338,14 @@ RSpec.describe API::Users do
|
||||||
expect(response).to match_response_schema('public_api/v4/user/basics')
|
expect(response).to match_response_schema('public_api/v4/user/basics')
|
||||||
expect(json_response.first.keys).not_to include 'is_admin'
|
expect(json_response.first.keys).not_to include 'is_admin'
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when admin" do
|
||||||
context 'exclude_internal param' do
|
context 'exclude_internal param' do
|
||||||
let_it_be(:internal_user) { User.alert_bot }
|
let_it_be(:internal_user) { User.alert_bot }
|
||||||
|
|
||||||
it 'returns all users when it is not set' do
|
it 'returns all users when it is not set' do
|
||||||
get api("/users?exclude_internal=false", user)
|
get api("/users?exclude_internal=false", admin)
|
||||||
|
|
||||||
expect(response).to match_response_schema('public_api/v4/user/basics')
|
expect(response).to match_response_schema('public_api/v4/user/basics')
|
||||||
expect(response).to include_pagination_headers
|
expect(response).to include_pagination_headers
|
||||||
|
|
|
@ -3,8 +3,10 @@
|
||||||
RSpec.shared_context 'UsersFinder#execute filter by project context' do
|
RSpec.shared_context 'UsersFinder#execute filter by project context' do
|
||||||
let_it_be(:normal_user) { create(:user, username: 'johndoe') }
|
let_it_be(:normal_user) { create(:user, username: 'johndoe') }
|
||||||
let_it_be(:admin_user) { create(:user, :admin, username: 'iamadmin') }
|
let_it_be(:admin_user) { create(:user, :admin, username: 'iamadmin') }
|
||||||
|
let_it_be(:banned_user) { create(:user, :banned, username: 'iambanned') }
|
||||||
let_it_be(:blocked_user) { create(:user, :blocked, username: 'notsorandom') }
|
let_it_be(:blocked_user) { create(:user, :blocked, username: 'notsorandom') }
|
||||||
let_it_be(:external_user) { create(:user, :external) }
|
let_it_be(:external_user) { create(:user, :external) }
|
||||||
|
let_it_be(:unconfirmed_user) { create(:user, confirmed_at: nil) }
|
||||||
let_it_be(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
|
let_it_be(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
|
||||||
let_it_be(:internal_user) { User.alert_bot }
|
let_it_be(:internal_user) { User.alert_bot.tap { |u| u.confirm } }
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue