Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-04-06 21:08:15 +00:00
parent da6cd333e7
commit 02c6800ac5
36 changed files with 751 additions and 176 deletions

View File

@ -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>

View File

@ -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;

View File

@ -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"

View File

@ -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,
};
} }
}); });
}, },

View File

@ -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"
> >

View File

@ -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>

View File

@ -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 {

View File

@ -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 {

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
fcf7a6569afb7fdb95834179df5632ad14165d27476eb020e9db07e504f75f32

View File

@ -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);

View File

@ -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
``` ```

View File

@ -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
``` ```

View File

@ -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
``` ```

View File

@ -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
``` ```

View File

@ -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
``` ```

View File

@ -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

View File

@ -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:

View File

@ -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).

View File

@ -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.

View File

@ -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]

View 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 ""

View File

@ -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)

View File

@ -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

View File

@ -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');
});
});
});

View File

@ -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', () => {

View File

@ -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}`, () => {

View File

@ -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', () => {

View File

@ -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)

View File

@ -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'

View File

@ -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

View File

@ -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