Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-12-09 12:15:43 +00:00
parent 3d233a67cf
commit 15f5da601b
61 changed files with 1146 additions and 367 deletions

View File

@ -220,6 +220,10 @@ export const FiltersInfo = {
types: { types: {
negatedSupport: true, negatedSupport: true,
}, },
confidential: {
negatedSupport: false,
transform: (val) => val === 'yes',
},
search: { search: {
negatedSupport: false, negatedSupport: false,
}, },

View File

@ -45,6 +45,7 @@ export default {
epicId, epicId,
myReactionEmoji, myReactionEmoji,
releaseTag, releaseTag,
confidential,
} = this.filterParams; } = this.filterParams;
const filteredSearchValue = []; const filteredSearchValue = [];
@ -113,6 +114,13 @@ export default {
}); });
} }
if (confidential !== undefined) {
filteredSearchValue.push({
type: 'confidential',
value: { data: confidential },
});
}
if (epicId) { if (epicId) {
filteredSearchValue.push({ filteredSearchValue.push({
type: 'epic', type: 'epic',
@ -211,6 +219,7 @@ export default {
myReactionEmoji, myReactionEmoji,
iterationId, iterationId,
releaseTag, releaseTag,
confidential,
} = this.filterParams; } = this.filterParams;
let notParams = {}; let notParams = {};
@ -245,6 +254,7 @@ export default {
epic_id: isGid(epicId) ? getIdFromGraphQLId(epicId) : epicId, epic_id: isGid(epicId) ? getIdFromGraphQLId(epicId) : epicId,
my_reaction_emoji: myReactionEmoji, my_reaction_emoji: myReactionEmoji,
release_tag: releaseTag, release_tag: releaseTag,
confidential,
}; };
}, },
}, },
@ -311,6 +321,9 @@ export default {
case 'release': case 'release':
filterParams.releaseTag = filter.value.data; filterParams.releaseTag = filter.value.data;
break; break;
case 'confidential':
filterParams.confidential = filter.value.data;
break;
case 'filtered-search-term': case 'filtered-search-term':
if (filter.value.data) plainText.push(filter.value.data); if (filter.value.data) plainText.push(filter.value.data);
break; break;

View File

@ -349,6 +349,9 @@ export default {
v-if="showCreate" v-if="showCreate"
v-gl-modal-directive="'board-config-modal'" v-gl-modal-directive="'board-config-modal'"
data-qa-selector="create_new_board_button" data-qa-selector="create_new_board_button"
data-track-action="click_button"
data-track-label="create_new_board"
data-track-property="dropdown"
@click.prevent="showPage('new')" @click.prevent="showPage('new')"
> >
{{ s__('IssueBoards|Create new board') }} {{ s__('IssueBoards|Create new board') }}

View File

@ -13,6 +13,7 @@ import { __ } from '~/locale';
import { import {
TOKEN_TITLE_MY_REACTION, TOKEN_TITLE_MY_REACTION,
OPERATOR_IS_AND_IS_NOT, OPERATOR_IS_AND_IS_NOT,
OPERATOR_IS_ONLY,
} from '~/vue_shared/components/filtered_search_bar/constants'; } from '~/vue_shared/components/filtered_search_bar/constants';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue'; import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
@ -36,6 +37,7 @@ export default {
issue: __('Issue'), issue: __('Issue'),
milestone: __('Milestone'), milestone: __('Milestone'),
release: __('Release'), release: __('Release'),
confidential: __('Confidential'),
}, },
components: { BoardFilteredSearch }, components: { BoardFilteredSearch },
inject: ['isSignedIn', 'releasesFetchPath'], inject: ['isSignedIn', 'releasesFetchPath'],
@ -68,6 +70,7 @@ export default {
type, type,
milestone, milestone,
release, release,
confidential,
} = this.$options.i18n; } = this.$options.i18n;
const { types } = this.$options; const { types } = this.$options;
const { fetchAuthors, fetchLabels } = issueBoardFilters( const { fetchAuthors, fetchLabels } = issueBoardFilters(
@ -132,6 +135,18 @@ export default {
}); });
}, },
}, },
{
type: 'confidential',
icon: 'eye-slash',
title: confidential,
unique: true,
token: GlFilteredSearchToken,
operators: OPERATOR_IS_ONLY,
options: [
{ icon: 'eye-slash', value: 'yes', title: __('Yes') },
{ icon: 'eye', value: 'no', title: __('No') },
],
},
] ]
: []), : []),
{ {

View File

@ -104,6 +104,7 @@ export const FilterFields = {
'assigneeUsername', 'assigneeUsername',
'assigneeWildcardId', 'assigneeWildcardId',
'authorUsername', 'authorUsername',
'confidential',
'labelName', 'labelName',
'milestoneTitle', 'milestoneTitle',
'milestoneWildcardId', 'milestoneWildcardId',

View File

@ -1,6 +1,5 @@
import { memoize } from 'lodash'; import { memoize } from 'lodash';
import { createNodeDict } from '../utils'; import { createNodeDict } from '../utils';
import { EXPLICIT_NEEDS_PROPERTY, NEEDS_PROPERTY } from '../constants';
import { createSankey } from './dag/drawing_utils'; import { createSankey } from './dag/drawing_utils';
/* /*
@ -16,14 +15,12 @@ const deduplicate = (item, itemIndex, arr) => {
return foundIdx === itemIndex; return foundIdx === itemIndex;
}; };
export const makeLinksFromNodes = (nodes, nodeDict, { needsKey = NEEDS_PROPERTY } = {}) => { export const makeLinksFromNodes = (nodes, nodeDict) => {
const constantLinkValue = 10; // all links are the same weight const constantLinkValue = 10; // all links are the same weight
return nodes return nodes
.map(({ jobs, name: groupName }) => .map(({ jobs, name: groupName }) =>
jobs.map((job) => { jobs.map(({ needs = [] }) =>
const needs = job[needsKey] || []; needs.reduce((acc, needed) => {
return needs.reduce((acc, needed) => {
// It's possible that we have an optional job, which // It's possible that we have an optional job, which
// is being needed by another job. In that scenario, // is being needed by another job. In that scenario,
// the needed job doesn't exist, so we don't want to // the needed job doesn't exist, so we don't want to
@ -37,8 +34,8 @@ export const makeLinksFromNodes = (nodes, nodeDict, { needsKey = NEEDS_PROPERTY
} }
return acc; return acc;
}, []); }, []),
}), ),
) )
.flat(2); .flat(2);
}; };
@ -79,9 +76,9 @@ export const filterByAncestors = (links, nodeDict) =>
return !allAncestors.includes(source); return !allAncestors.includes(source);
}); });
export const parseData = (nodes, { needsKey = NEEDS_PROPERTY } = {}) => { export const parseData = (nodes) => {
const nodeDict = createNodeDict(nodes, { needsKey }); const nodeDict = createNodeDict(nodes);
const allLinks = makeLinksFromNodes(nodes, nodeDict, { needsKey }); const allLinks = makeLinksFromNodes(nodes, nodeDict);
const filteredLinks = allLinks.filter(deduplicate); const filteredLinks = allLinks.filter(deduplicate);
const links = filterByAncestors(filteredLinks, nodeDict); const links = filterByAncestors(filteredLinks, nodeDict);
@ -126,8 +123,7 @@ export const removeOrphanNodes = (sankeyfiedNodes) => {
export const listByLayers = ({ stages }) => { export const listByLayers = ({ stages }) => {
const arrayOfJobs = stages.flatMap(({ groups }) => groups); const arrayOfJobs = stages.flatMap(({ groups }) => groups);
const parsedData = parseData(arrayOfJobs); const parsedData = parseData(arrayOfJobs);
const explicitParsedData = parseData(arrayOfJobs, { needsKey: EXPLICIT_NEEDS_PROPERTY }); const dataWithLayers = createSankey()(parsedData);
const dataWithLayers = createSankey()(explicitParsedData);
const pipelineLayers = dataWithLayers.nodes.reduce((acc, { layer, name }) => { const pipelineLayers = dataWithLayers.nodes.reduce((acc, { layer, name }) => {
/* sort groups by layer */ /* sort groups by layer */

View File

@ -1,5 +1,4 @@
import { reportToSentry } from '../utils'; import { reportToSentry } from '../utils';
import { EXPLICIT_NEEDS_PROPERTY, NEEDS_PROPERTY } from '../constants';
const unwrapGroups = (stages) => { const unwrapGroups = (stages) => {
return stages.map((stage, idx) => { return stages.map((stage, idx) => {
@ -28,16 +27,12 @@ const unwrapNodesWithName = (jobArray, prop, field = 'name') => {
} }
return jobArray.map((job) => { return jobArray.map((job) => {
if (job[prop]) { return { ...job, [prop]: job[prop].nodes.map((item) => item[field] || '') };
return { ...job, [prop]: job[prop].nodes.map((item) => item[field] || '') };
}
return job;
}); });
}; };
const unwrapJobWithNeeds = (denodedJobArray) => { const unwrapJobWithNeeds = (denodedJobArray) => {
const explicitNeedsUnwrapped = unwrapNodesWithName(denodedJobArray, EXPLICIT_NEEDS_PROPERTY); return unwrapNodesWithName(denodedJobArray, 'needs');
return unwrapNodesWithName(explicitNeedsUnwrapped, NEEDS_PROPERTY);
}; };
const unwrapStagesWithNeedsAndLookup = (denodedStages) => { const unwrapStagesWithNeedsAndLookup = (denodedStages) => {

View File

@ -7,8 +7,6 @@ export const ANY_TRIGGER_AUTHOR = 'Any';
export const SUPPORTED_FILTER_PARAMETERS = ['username', 'ref', 'status', 'source']; export const SUPPORTED_FILTER_PARAMETERS = ['username', 'ref', 'status', 'source'];
export const FILTER_TAG_IDENTIFIER = 'tag'; export const FILTER_TAG_IDENTIFIER = 'tag';
export const SCHEDULE_ORIGIN = 'schedule'; export const SCHEDULE_ORIGIN = 'schedule';
export const NEEDS_PROPERTY = 'needs';
export const EXPLICIT_NEEDS_PROPERTY = 'previousStageJobsOrNeeds';
export const TestStatus = { export const TestStatus = {
FAILED: 'failed', FAILED: 'failed',

View File

@ -1,6 +1,6 @@
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { pickBy } from 'lodash'; import { pickBy } from 'lodash';
import { SUPPORTED_FILTER_PARAMETERS, NEEDS_PROPERTY } from './constants'; import { SUPPORTED_FILTER_PARAMETERS } from './constants';
/* /*
The following functions are the main engine in transforming the data as The following functions are the main engine in transforming the data as
@ -35,11 +35,11 @@ import { SUPPORTED_FILTER_PARAMETERS, NEEDS_PROPERTY } from './constants';
10 -> value (constant) 10 -> value (constant)
*/ */
export const createNodeDict = (nodes, { needsKey = NEEDS_PROPERTY } = {}) => { export const createNodeDict = (nodes) => {
return nodes.reduce((acc, node) => { return nodes.reduce((acc, node) => {
const newNode = { const newNode = {
...node, ...node,
needs: node.jobs.map((job) => job[needsKey] || []).flat(), needs: node.jobs.map((job) => job.needs || []).flat(),
}; };
if (node.size > 1) { if (node.size > 1) {

View File

@ -110,7 +110,7 @@ export default {
<template> <template>
<div <div
v-gl-tooltip="tooltipOptions" v-gl-tooltip="tooltipOptions"
:class="{ 'multiple-users': hasMoreThanOneAssignee }" :class="{ 'multiple-users gl-relative': hasMoreThanOneAssignee }"
:title="tooltipTitle" :title="tooltipTitle"
class="sidebar-collapsed-icon sidebar-collapsed-user" class="sidebar-collapsed-icon sidebar-collapsed-user"
> >

View File

@ -89,7 +89,7 @@ export default {
<template> <template>
<div <div
v-gl-tooltip="tooltipOptions" v-gl-tooltip="tooltipOptions"
:class="{ 'multiple-users': hasMoreThanOneReviewer }" :class="{ 'multiple-users gl-relative': hasMoreThanOneReviewer }"
:title="tooltipTitle" :title="tooltipTitle"
class="sidebar-collapsed-icon sidebar-collapsed-user" class="sidebar-collapsed-icon sidebar-collapsed-user"
> >

View File

@ -456,12 +456,6 @@
} }
.multiple-users { .multiple-users {
position: relative;
height: 24px;
margin-bottom: 17px;
margin-top: 4px;
padding-bottom: 4px;
.btn-link { .btn-link {
padding: 0; padding: 0;
border: 0; border: 0;

View File

@ -9,6 +9,8 @@ class AutocompleteController < ApplicationController
feature_category :code_review, [:merge_request_target_branches] feature_category :code_review, [:merge_request_target_branches]
feature_category :continuous_delivery, [:deploy_keys_with_owners] feature_category :continuous_delivery, [:deploy_keys_with_owners]
urgency :low, [:merge_request_target_branches]
def users def users
group = Autocomplete::GroupFinder group = Autocomplete::GroupFinder
.new(current_user, project, params) .new(current_user, project, params)

View File

@ -18,6 +18,8 @@ class DashboardController < Dashboard::ApplicationController
feature_category :team_planning, [:issues, :issues_calendar] feature_category :team_planning, [:issues, :issues_calendar]
feature_category :code_review, [:merge_requests] feature_category :code_review, [:merge_requests]
urgency :low, [:merge_requests]
def activity def activity
respond_to do |format| respond_to do |format|
format.html format.html

View File

@ -5,6 +5,8 @@ class Groups::AutocompleteSourcesController < Groups::ApplicationController
feature_category :team_planning, [:issues, :labels, :milestones, :commands] feature_category :team_planning, [:issues, :labels, :milestones, :commands]
feature_category :code_review, [:merge_requests] feature_category :code_review, [:merge_requests]
urgency :low, [:merge_requests]
def members def members
render json: ::Groups::ParticipantsService.new(@group, current_user).execute(target) render json: ::Groups::ParticipantsService.new(@group, current_user).execute(target)
end end

View File

@ -60,6 +60,7 @@ class GroupsController < Groups::ApplicationController
feature_category :importers, [:export, :download_export] feature_category :importers, [:export, :download_export]
urgency :high, [:unfoldered_environment_names] urgency :high, [:unfoldered_environment_names]
urgency :low, [:merge_requests]
def index def index
redirect_to(current_user ? dashboard_groups_path : explore_groups_path) redirect_to(current_user ? dashboard_groups_path : explore_groups_path)

View File

@ -8,6 +8,8 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
feature_category :users, [:members] feature_category :users, [:members]
feature_category :snippets, [:snippets] feature_category :snippets, [:snippets]
urgency :low, [:merge_requests]
def members def members
render json: ::Projects::ParticipantsService.new(@project, current_user).execute(target) render json: ::Projects::ParticipantsService.new(@project, current_user).execute(target)
end end

View File

@ -5,6 +5,12 @@ class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::Ap
before_action :authorize_can_resolve_conflicts! before_action :authorize_can_resolve_conflicts!
urgency :low, [
:show,
:conflict_for_path,
:resolve_conflicts
]
def show def show
respond_to do |format| respond_to do |format|
format.html do format.html do

View File

@ -13,6 +13,11 @@ class Projects::MergeRequests::ContentController < Projects::MergeRequests::Appl
FAST_POLLING_INTERVAL = 10.seconds.in_milliseconds FAST_POLLING_INTERVAL = 10.seconds.in_milliseconds
SLOW_POLLING_INTERVAL = 5.minutes.in_milliseconds SLOW_POLLING_INTERVAL = 5.minutes.in_milliseconds
urgency :low, [
:widget,
:cached_widget
]
def widget def widget
respond_to do |format| respond_to do |format|
format.json do format.json do

View File

@ -10,6 +10,15 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path] before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path]
before_action :build_merge_request, except: [:create] before_action :build_merge_request, except: [:create]
urgency :low, [
:new,
:create,
:pipelines,
:diffs,
:branch_from,
:branch_to
]
def new def new
define_new_vars define_new_vars
end end

View File

@ -14,6 +14,13 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
after_action :track_viewed_diffs_events, only: [:diffs_batch] after_action :track_viewed_diffs_events, only: [:diffs_batch]
urgency :low, [
:show,
:diff_for_path,
:diffs_batch,
:diffs_metadata
]
def show def show
render_diffs render_diffs
end end

View File

@ -9,6 +9,13 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
before_action :authorize_admin_draft!, only: [:update, :destroy] before_action :authorize_admin_draft!, only: [:update, :destroy]
before_action :authorize_admin_draft!, if: -> { action_name == 'publish' && params[:id].present? } before_action :authorize_admin_draft!, if: -> { action_name == 'publish' && params[:id].present? }
urgency :low, [
:create,
:update,
:destroy,
:publish
]
def index def index
drafts = prepare_notes_for_rendering(draft_notes) drafts = prepare_notes_for_rendering(draft_notes)
render json: DraftNoteSerializer.new(current_user: current_user).represent(drafts) render json: DraftNoteSerializer.new(current_user: current_user).represent(drafts)

View File

@ -71,6 +71,21 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
feature_category :continuous_integration, [:pipeline_status, :pipelines, :exposed_artifacts] feature_category :continuous_integration, [:pipeline_status, :pipelines, :exposed_artifacts]
urgency :high, [:export_csv] urgency :high, [:export_csv]
urgency :low, [
:index,
:show,
:commits,
:bulk_update,
:edit,
:update,
:cancel_auto_merge,
:merge,
:ci_environments_status,
:destroy,
:rebase,
:discussions,
:description_diff
]
def index def index
@merge_requests = @issuables @merge_requests = @issuables

View File

@ -12,7 +12,6 @@ class SearchController < ApplicationController
around_action :allow_gitaly_ref_name_caching around_action :allow_gitaly_ref_name_caching
before_action :block_anonymous_global_searches, :check_scope_global_search_enabled, except: :opensearch before_action :block_anonymous_global_searches, :check_scope_global_search_enabled, except: :opensearch
before_action :strip_surrounding_whitespace_from_search, except: :opensearch
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
requires_cross_project_access if: -> do requires_cross_project_access if: -> do
search_term_present = params[:search].present? || params[:term].present? search_term_present = params[:search].present? || params[:term].present?
@ -93,12 +92,12 @@ class SearchController < ApplicationController
def search_term_valid? def search_term_valid?
unless search_service.valid_query_length? unless search_service.valid_query_length?
flash[:alert] = t('errors.messages.search_chars_too_long', count: SearchService::SEARCH_CHAR_LIMIT) flash[:alert] = t('errors.messages.search_chars_too_long', count: Gitlab::Search::Params::SEARCH_CHAR_LIMIT)
return false return false
end end
unless search_service.valid_terms_count? unless search_service.valid_terms_count?
flash[:alert] = t('errors.messages.search_terms_too_long', count: SearchService::SEARCH_TERM_LIMIT) flash[:alert] = t('errors.messages.search_terms_too_long', count: Gitlab::Search::Params::SEARCH_TERM_LIMIT)
return false return false
end end
@ -143,6 +142,11 @@ class SearchController < ApplicationController
payload[:metadata]['meta.search.filters.confidential'] = params[:confidential] payload[:metadata]['meta.search.filters.confidential'] = params[:confidential]
payload[:metadata]['meta.search.filters.state'] = params[:state] payload[:metadata]['meta.search.filters.state'] = params[:state]
payload[:metadata]['meta.search.force_search_results'] = params[:force_search_results] payload[:metadata]['meta.search.force_search_results'] = params[:force_search_results]
if search_service.abuse_detected?
payload[:metadata]['abuse.confidence'] = Gitlab::Abuse.confidence(:certain)
payload[:metadata]['abuse.messages'] = search_service.abuse_messages
end
end end
def block_anonymous_global_searches def block_anonymous_global_searches
@ -194,10 +198,6 @@ class SearchController < ApplicationController
render status: :request_timeout render status: :request_timeout
end end
end end
def strip_surrounding_whitespace_from_search
%i(term search).each { |param| params[param]&.strip! }
end
end end
SearchController.prepend_mod_with('SearchController') SearchController.prepend_mod_with('SearchController')

View File

@ -91,14 +91,6 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) {
name name
} }
} }
previousStageJobsOrNeeds {
__typename
nodes {
__typename
id
name
}
}
status: detailedStatus { status: detailedStatus {
__typename __typename
id id

View File

@ -168,6 +168,24 @@ module RelativePositioning
self.relative_position = MIN_POSITION self.relative_position = MIN_POSITION
end end
def next_object_by_relative_position(ignoring: nil, order: :asc)
relation = relative_positioning_scoped_items(ignoring: ignoring).reorder(relative_position: order)
relation = if order == :asc
relation.where(self.class.arel_table[:relative_position].gt(relative_position))
else
relation.where(self.class.arel_table[:relative_position].lt(relative_position))
end
relation.first
end
def relative_positioning_scoped_items(ignoring: nil)
relation = self.class.relative_positioning_query_base(self)
relation = exclude_self(relation, excluded: ignoring) if ignoring.present?
relation
end
# This method is used during rebalancing - override it to customise the update # This method is used during rebalancing - override it to customise the update
# logic: # logic:
def update_relative_siblings(relation, range, delta) def update_relative_siblings(relation, range, delta)

View File

@ -229,9 +229,37 @@ class Issue < ApplicationRecord
end end
end end
def next_object_by_relative_position(ignoring: nil, order: :asc)
return super unless Feature.enabled?(:optimized_issue_neighbor_queries, project, default_enabled: :yaml)
array_mapping_scope = -> (id_expression) do
relation = Issue.where(Issue.arel_table[:project_id].eq(id_expression))
if order == :asc
relation.where(Issue.arel_table[:relative_position].gt(relative_position))
else
relation.where(Issue.arel_table[:relative_position].lt(relative_position))
end
end
relation = Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new(
scope: Issue.order(relative_position: order, id: order),
array_scope: relative_positioning_parent_projects,
array_mapping_scope: array_mapping_scope,
finder_query: -> (_, id_expression) { Issue.where(Issue.arel_table[:id].eq(id_expression)) }
).execute
relation = exclude_self(relation, excluded: ignoring) if ignoring.present?
relation.take
end
def relative_positioning_parent_projects
project.group&.root_ancestor&.all_projects&.select(:id) || Project.id_in(project).select(:id)
end
def self.relative_positioning_query_base(issue) def self.relative_positioning_query_base(issue)
projects = issue.project.group&.root_ancestor&.all_projects || issue.project in_projects(issue.relative_positioning_parent_projects)
in_projects(projects)
end end
def self.relative_positioning_parent_column def self.relative_positioning_parent_column

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class GroupPolicy < BasePolicy class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
include FindGroupProjects include FindGroupProjects
desc "Group is public" desc "Group is public"

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
module Namespaces
class GroupProjectNamespaceSharedPolicy < ::NamespacePolicy
# Nothing here at the moment, but as we move policies from ProjectPolicy to ProjectNamespacePolicy,
# anything common with GroupPolicy but not with UserNamespacePolicy can go in here.
# See https://gitlab.com/groups/gitlab-org/-/epics/6689
end
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
module Namespaces module Namespaces
class ProjectNamespacePolicy < NamespacePolicy class ProjectNamespacePolicy < Namespaces::GroupProjectNamespaceSharedPolicy
# For now users are not granted any permissions on project namespace # For now users are not granted any permissions on project namespace
# as it's completely hidden to them. When we start using project # as it's completely hidden to them. When we start using project
# namespaces in queries, we will have to extend this policy. # namespaces in queries, we will have to extend this policy.

View File

@ -89,11 +89,9 @@ module Ci
end end
def runner_projects_relation def runner_projects_relation
if ::Feature.enabled?(:ci_pending_builds_project_runners_decoupling, runner, default_enabled: :yaml) runner
runner.runner_projects.select('"ci_runner_projects"."project_id"::bigint') .runner_projects
else .select('"ci_runner_projects"."project_id"::bigint')
runner.projects.without_deleted.with_builds_enabled
end
end end
end end
end end

View File

@ -269,14 +269,7 @@ module Ci
{ {
missing_dependency_failure: -> (build, _) { !build.has_valid_build_dependencies? }, missing_dependency_failure: -> (build, _) { !build.has_valid_build_dependencies? },
runner_unsupported: -> (build, params) { !build.supported_runner?(params.dig(:info, :features)) }, runner_unsupported: -> (build, params) { !build.supported_runner?(params.dig(:info, :features)) },
archived_failure: -> (build, _) { build.archived? } archived_failure: -> (build, _) { build.archived? },
}.merge(builds_enabled_checks)
end
def builds_enabled_checks
return {} unless ::Feature.enabled?(:ci_queueing_builds_enabled_checks, runner, default_enabled: :yaml)
{
project_deleted: -> (build, _) { build.project.pending_delete? }, project_deleted: -> (build, _) { build.project.pending_delete? },
builds_disabled: -> (build, _) { !build.project.builds_enabled? } builds_disabled: -> (build, _) { !build.project.builds_enabled? }
} }

View File

@ -2,42 +2,35 @@
class SearchService class SearchService
include Gitlab::Allowable include Gitlab::Allowable
include Gitlab::Utils::StrongMemoize
SEARCH_TERM_LIMIT = 64
SEARCH_CHAR_LIMIT = 4096
DEFAULT_PER_PAGE = Gitlab::SearchResults::DEFAULT_PER_PAGE DEFAULT_PER_PAGE = Gitlab::SearchResults::DEFAULT_PER_PAGE
MAX_PER_PAGE = 200 MAX_PER_PAGE = 200
def initialize(current_user, params = {}) def initialize(current_user, params = {})
@current_user = current_user @current_user = current_user
@params = params.dup @params = Gitlab::Search::Params.new(params, detect_abuse: prevent_abusive_searches?)
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def project def project
return @project if defined?(@project) strong_memoize(:project) do
if params[:project_id].present? && valid_request?
@project =
if params[:project_id].present?
the_project = Project.find_by(id: params[:project_id]) the_project = Project.find_by(id: params[:project_id])
can?(current_user, :read_project, the_project) ? the_project : nil can?(current_user, :read_project, the_project) ? the_project : nil
else
nil
end end
end
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def group def group
return @group if defined?(@group) strong_memoize(:group) do
if params[:group_id].present? && valid_request?
@group =
if params[:group_id].present?
the_group = Group.find_by(id: params[:group_id]) the_group = Group.find_by(id: params[:group_id])
can?(current_user, :read_group, the_group) ? the_group : nil can?(current_user, :read_group, the_group) ? the_group : nil
else
nil
end end
end
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
@ -55,18 +48,13 @@ class SearchService
@show_snippets = params[:snippets] == 'true' @show_snippets = params[:snippets] == 'true'
end end
def valid_query_length?
params[:search].length <= SEARCH_CHAR_LIMIT
end
def valid_terms_count?
params[:search].split.count { |word| word.length >= 3 } <= SEARCH_TERM_LIMIT
end
delegate :scope, to: :search_service delegate :scope, to: :search_service
delegate :valid_terms_count?, :valid_query_length?, to: :params
def search_results def search_results
@search_results ||= search_service.execute strong_memoize(:search_results) do
abuse_detected? ? Gitlab::EmptySearchResults.new : search_service.execute
end
end end
def search_objects(preload_method = nil) def search_objects(preload_method = nil)
@ -83,8 +71,30 @@ class SearchService
search_results.aggregations(scope) search_results.aggregations(scope)
end end
def abuse_detected?
strong_memoize(:abuse_detected) do
params.abusive?
end
end
def abuse_messages
return [] unless params.abusive?
params.abuse_detection.errors.messages
end
def valid_request?
strong_memoize(:valid_request) do
params.valid?
end
end
private private
def prevent_abusive_searches?
Feature.enabled?(:prevent_abusive_searches, current_user)
end
def page def page
[1, params[:page].to_i].max [1, params[:page].to_i].max
end end

View File

@ -9,7 +9,7 @@
%a.js-retry-load{ href: '#' } %a.js-retry-load{ href: '#' }
= s_('UserProfile|Retry') = s_('UserProfile|Retry')
.user-calendar-activities .user-calendar-activities
- if @user.user_readme - if @user.user_readme&.rich_viewer
.row.justify-content-center .row.justify-content-center
.col-12.col-md-10.col-lg-8.gl-my-6 .col-12.col-md-10.col-lg-8.gl-my-6
.gl-display-flex .gl-display-flex

View File

@ -1,8 +1,8 @@
--- ---
name: ci_queueing_builds_enabled_checks name: optimized_issue_neighbor_queries
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70581 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76073
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341131 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/345921
milestone: '14.4' milestone: '14.6'
type: development type: development
group: group::pipeline execution group: group::project management
default_enabled: false default_enabled: false

View File

@ -1,8 +1,8 @@
--- ---
name: ci_pending_builds_project_runners_decoupling name: prevent_abusive_searches
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70415 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74953
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341005 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/346263
milestone: '14.4' milestone: '14.6'
type: development type: development
group: group::pipeline execution group: group::global search
default_enabled: false default_enabled: false

View File

@ -0,0 +1,14 @@
- name: "Remove `type` and `types` keyword in CI/CD configuration" # The name of the feature to be deprecated
announcement_milestone: "14.6" # The milestone when this feature was first announced as deprecated.
announcement_date: "2021-12-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_milestone: "15.0" # The milestone when this feature is planned to be removed
body: | # Do not modify this line, instead modify the lines below.
The `type` and `types` CI/CD keywords will be removed in GitLab 15.0. Pipelines that use these keywords will stop working, so you must switch to `stage` and `stages`, which have the same behavior.
# The following items are not published on the docs page, but may be used in the future.
stage: # (optional - may be required in the future) String value of the stage that the feature was created in. e.g., Growth
tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
issue_url: # (optional) This is a link to the deprecation issue in GitLab
documentation_url: # (optional) This is a link to the current documentation page
image_url: # (optional) This is a link to a thumbnail image depicting the feature
video_url: # (optional) Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
removal_date: # (optional - may be required in the future) YYYY-MM-DD format. This should almost always be the 22nd of a month (YYYY-MM-22), the date of the milestone release when this feature is planned to be removed

View File

@ -268,7 +268,7 @@ sudo -u git -H bundle exec rake gitlab:tcp_check[example.com,80] RAILS_ENV=produ
GitLab uses a shared lock mechanism: `ExclusiveLease` to prevent simultaneous operations GitLab uses a shared lock mechanism: `ExclusiveLease` to prevent simultaneous operations
in a shared resource. An example is running periodic garbage collection on repositories. in a shared resource. An example is running periodic garbage collection on repositories.
In very specific situations, a operation locked by an Exclusive Lease can fail without In very specific situations, an operation locked by an Exclusive Lease can fail without
releasing the lock. If you can't wait for it to expire, you can run this task to manually releasing the lock. If you can't wait for it to expire, you can run this task to manually
clear it. clear it.

View File

@ -213,6 +213,12 @@ In GitLab 14.5, we introduced the command `gitlab-ctl promote` to promote any Ge
Announced: 2021-11-22 Announced: 2021-11-22
### Remove `type` and `types` keyword in CI/CD configuration
The `type` and `types` CI/CD keywords will be removed in GitLab 15.0. Pipelines that use these keywords will stop working, so you must switch to `stage` and `stages`, which have the same behavior.
Announced: 2021-12-22
### Remove the `:dependency_proxy_for_private_groups` feature flag ### Remove the `:dependency_proxy_for_private_groups` feature flag
We added a feature flag because [GitLab-#11582](https://gitlab.com/gitlab-org/gitlab/-/issues/11582) changed how public groups use the Dependency Proxy. Prior to this change, you could use the Dependency Proxy without authentication. The change requires authentication to use the Dependency Proxy. We added a feature flag because [GitLab-#11582](https://gitlab.com/gitlab-org/gitlab/-/issues/11582) changed how public groups use the Dependency Proxy. Prior to this change, you could use the Dependency Proxy without authentication. The change requires authentication to use the Dependency Proxy.

View File

@ -42,6 +42,11 @@ system note in the issue's comments.
![Confidential issues system notes](img/confidential_issues_system_notes.png) ![Confidential issues system notes](img/confidential_issues_system_notes.png)
When an issue is made confidential, only users with at least the [Reporter role](../../permissions.md)
for the project have access to the issue.
Users with Guest or [Minimal](../../permissions.md#users-with-minimal-access) roles can't access
the issue even if they were actively participating before the change.
## Indications of a confidential issue ## Indications of a confidential issue
There are a few things that visually separate a confidential issue from a There are a few things that visually separate a confidential issue from a

18
lib/gitlab/abuse.rb Normal file
View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
module Gitlab
module Abuse
CONFIDENCE_LEVELS = {
certain: 1.0,
likely: 0.8,
uncertain: 0.5,
unknown: 0.0
}.freeze
class << self
def confidence(rating)
CONFIDENCE_LEVELS.fetch(rating.to_sym)
end
end
end
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
module Gitlab
# This class has the same interface as SearchResults except
# it is empty and does not do any work.
#
# We use this when responding to abusive search requests.
class EmptySearchResults
def initialize(*)
end
def objects(*)
Kaminari.paginate_array([])
end
def formatted_count(*)
'0'
end
def highlight_map(*)
{}
end
def aggregations(*)
[]
end
end
end

View File

@ -66,19 +66,11 @@ module Gitlab
end end
def lhs_neighbour def lhs_neighbour
scoped_items neighbour(object.next_object_by_relative_position(ignoring: ignoring, order: :desc))
.where('relative_position < ?', relative_position)
.reorder(relative_position: :desc)
.first
.then { |x| neighbour(x) }
end end
def rhs_neighbour def rhs_neighbour
scoped_items neighbour(object.next_object_by_relative_position(ignoring: ignoring, order: :asc))
.where('relative_position > ?', relative_position)
.reorder(relative_position: :asc)
.first
.then { |x| neighbour(x) }
end end
def neighbour(item) def neighbour(item)
@ -87,12 +79,6 @@ module Gitlab
self.class.new(item, range, ignoring: ignoring) self.class.new(item, range, ignoring: ignoring)
end end
def scoped_items
r = model_class.relative_positioning_query_base(object)
r = object.exclude_self(r, excluded: ignoring) if ignoring.present?
r
end
def calculate_relative_position(calculation) def calculate_relative_position(calculation)
# When calculating across projects, this is much more efficient than # When calculating across projects, this is much more efficient than
# MAX(relative_position) without the GROUP BY, due to index usage: # MAX(relative_position) without the GROUP BY, due to index usage:
@ -186,6 +172,10 @@ module Gitlab
Gap.new(gap.first, gap.second || default_end) Gap.new(gap.first, gap.second || default_end)
end end
def scoped_items
object.relative_positioning_scoped_items(ignoring: ignoring)
end
def relative_position def relative_position
object.relative_position object.relative_position
end end

View File

@ -0,0 +1,81 @@
# frozen_string_literal: true
module Gitlab
module Search
class AbuseDetection
include ActiveModel::Validations
include AbuseValidators
ABUSIVE_TERM_SIZE = 100
ALLOWED_CHARS_REGEX = %r{\A[[:alnum:]_\-\/\.!]+\z}.freeze
MINIMUM_SEARCH_CHARS = 2
ALLOWED_SCOPES = %w(
blobs
code
commits
epics
issues
merge_requests
milestones
notes
projects
snippet_titles
users
wiki_blobs
).freeze
READABLE_PARAMS = %i(
group_id
project_id
project_ref
query_string
repository_ref
scope
).freeze
STOP_WORDS = %w(
a an and are as at be but by for if in into is it no not of on or such that the their then there these they this to was will with
).freeze
validates :project_id, :group_id,
numericality: { only_integer: true, message: "abusive ID detected" }, allow_blank: true
validates :scope, inclusion: { in: ALLOWED_SCOPES, message: 'abusive scope detected' }, allow_blank: true
validates :repository_ref, :project_ref,
format: { with: ALLOWED_CHARS_REGEX, message: "abusive characters detected" }, allow_blank: true
validates :query_string,
exclusion: { in: STOP_WORDS, message: 'stopword only abusive search detected' }, allow_blank: true
validates :query_string,
length: { minimum: MINIMUM_SEARCH_CHARS, message: 'abusive tiny search detected' }, unless: :skip_tiny_search_validation?, allow_blank: true
validates :query_string,
no_abusive_term_length: { maximum: ABUSIVE_TERM_SIZE, maximum_for_url: ABUSIVE_TERM_SIZE * 2 }
validates :query_string, :repository_ref, :project_ref, no_abusive_coercion_from_string: true
attr_reader(*READABLE_PARAMS)
def initialize(params)
READABLE_PARAMS.each { |p| instance_variable_set("@#{p}", params[p]) }
end
private
def skip_tiny_search_validation?
wildcard_search? || stop_word_search?
end
def wildcard_search?
query_string == '*'
end
def stop_word_search?
STOP_WORDS.include? query_string
end
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module Gitlab
module Search
module AbuseValidators
class NoAbusiveCoercionFromStringValidator < ActiveModel::EachValidator
def validate_each(instance, attribute, value)
if value.present? && !value.is_a?(String)
instance.errors.add attribute, "abusive coercion from string detected"
end
end
end
end
end
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
module Gitlab
module Search
module AbuseValidators
class NoAbusiveTermLengthValidator < ActiveModel::EachValidator
def validate_each(instance, attribute, value)
return unless value.is_a?(String)
if value.split.any? { |term| term_too_long?(term) }
instance.errors.add attribute, 'abusive term length detected'
end
end
private
def term_too_long?(term)
char_limit = url_detected?(term) ? maximum_for_url : maximum
term.length >= char_limit
end
def url_detected?(uri_str)
URI::DEFAULT_PARSER.regexp[:ABS_URI].match? uri_str
end
def maximum_for_url
options.fetch(:maximum_for_url, maximum)
end
def maximum
options.fetch(:maximum)
end
end
end
end
end

View File

@ -0,0 +1,89 @@
# frozen_string_literal: true
module Gitlab
module Search
class Params
include ActiveModel::Validations
SEARCH_CHAR_LIMIT = 4096
SEARCH_TERM_LIMIT = 64
# Generic validation
validates :query_string, length: { maximum: SEARCH_CHAR_LIMIT }
validate :not_too_many_terms
attr_reader :raw_params, :query_string, :abuse_detection
alias_method :search, :query_string
alias_method :term, :query_string
def initialize(params, detect_abuse: true)
@raw_params = params.is_a?(Hash) ? params.with_indifferent_access : params.dup
@query_string = strip_surrounding_whitespace(@raw_params[:search] || @raw_params[:term])
@detect_abuse = detect_abuse
@abuse_detection = AbuseDetection.new(self) if @detect_abuse
validate
end
def [](key)
if respond_to? key
# We have this logic here to support reading custom attributes
# like @query_string
#
# This takes precedence over values in @raw_params
public_send(key) # rubocop:disable GitlabSecurity/PublicSend
else
raw_params[key]
end
end
def abusive?
detect_abuse? && abuse_detection.errors.any?
end
def valid_query_length?
return true unless errors.has_key? :query_string
errors[:query_string].none? { |msg| msg.include? SEARCH_CHAR_LIMIT.to_s }
end
def valid_terms_count?
return true unless errors.has_key? :query_string
errors[:query_string].none? { |msg| msg.include? SEARCH_TERM_LIMIT.to_s }
end
def validate
if detect_abuse?
abuse_detection.validate
end
super
end
def valid?
if detect_abuse?
abuse_detection.valid? && super
else
super
end
end
private
def detect_abuse?
@detect_abuse
end
def not_too_many_terms
if query_string.split.count { |word| word.length >= 3 } > SEARCH_TERM_LIMIT
errors.add :query_string, "has too many search terms (maximum is #{SEARCH_TERM_LIMIT})"
end
end
def strip_surrounding_whitespace(obj)
obj.to_s.strip
end
end
end
end

View File

@ -127,21 +127,26 @@ RSpec.describe SearchController do
context 'check search term length' do context 'check search term length' do
let(:search_queries) do let(:search_queries) do
char_limit = SearchService::SEARCH_CHAR_LIMIT char_limit = Gitlab::Search::Params::SEARCH_CHAR_LIMIT
term_limit = SearchService::SEARCH_TERM_LIMIT term_limit = Gitlab::Search::Params::SEARCH_TERM_LIMIT
term_char_limit = Gitlab::Search::AbuseDetection::ABUSIVE_TERM_SIZE
{ {
chars_under_limit: ('a' * (char_limit - 1)), chars_under_limit: (('a' * (term_char_limit - 1) + ' ') * (term_limit - 1))[0, char_limit],
chars_over_limit: ('a' * (char_limit + 1)), chars_over_limit: (('a' * (term_char_limit - 1) + ' ') * (term_limit - 1))[0, char_limit + 1],
terms_under_limit: ('abc ' * (term_limit - 1)), terms_under_limit: ('abc ' * (term_limit - 1)),
terms_over_limit: ('abc ' * (term_limit + 1)) terms_over_limit: ('abc ' * (term_limit + 1)),
term_length_over_limit: ('a' * (term_char_limit + 1)),
term_length_under_limit: ('a' * (term_char_limit - 1))
} }
end end
where(:string_name, :expectation) do where(:string_name, :expectation) do
:chars_under_limit | :not_to_set_flash :chars_under_limit | :not_to_set_flash
:chars_over_limit | :set_chars_flash :chars_over_limit | :set_chars_flash
:terms_under_limit | :not_to_set_flash :terms_under_limit | :not_to_set_flash
:terms_over_limit | :set_terms_flash :terms_over_limit | :set_terms_flash
:term_length_under_limit | :not_to_set_flash
:term_length_over_limit | :not_to_set_flash # abuse, so do nothing.
end end
with_them do with_them do
@ -187,6 +192,14 @@ RSpec.describe SearchController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
end end
context 'handling abusive search_terms' do
it 'succeeds but does NOT do anything' do
get :show, params: { scope: 'projects', search: '*', repository_ref: '-1%20OR%203%2B640-640-1=0%2B0%2B0%2B1' }
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:search_results)).to be_a Gitlab::EmptySearchResults
end
end
end end
context 'tab feature flags' do context 'tab feature flags' do
@ -221,16 +234,6 @@ RSpec.describe SearchController do
end end
end end
it 'strips surrounding whitespace from search query' do
get :show, params: { scope: 'notes', search: ' foobar ' }
expect(assigns[:search_term]).to eq 'foobar'
end
it 'strips surrounding whitespace from autocomplete term' do
expect(controller).to receive(:search_autocomplete_opts).with('youcompleteme')
get :autocomplete, params: { term: ' youcompleteme ' }
end
it 'finds issue comments' do it 'finds issue comments' do
project = create(:project, :public) project = create(:project, :public)
note = create(:note_on_issue, project: project) note = create(:note_on_issue, project: project)
@ -289,7 +292,7 @@ RSpec.describe SearchController do
end end
end end
describe 'GET #count' do describe 'GET #count', :aggregate_failures do
it_behaves_like 'when the user cannot read cross project', :count, { search: 'hello', scope: 'projects' } it_behaves_like 'when the user cannot read cross project', :count, { search: 'hello', scope: 'projects' }
it_behaves_like 'with external authorization service enabled', :count, { search: 'hello', scope: 'projects' } it_behaves_like 'with external authorization service enabled', :count, { search: 'hello', scope: 'projects' }
it_behaves_like 'support for active record query timeouts', :count, { search: 'hello', scope: 'projects' }, :search_results, :json it_behaves_like 'support for active record query timeouts', :count, { search: 'hello', scope: 'projects' }, :search_results, :json
@ -323,12 +326,38 @@ RSpec.describe SearchController do
expect(response.headers['Cache-Control']).to eq('private, no-store') expect(response.headers['Cache-Control']).to eq('private, no-store')
end end
it 'does NOT blow up if search param is NOT a string' do
get :count, params: { search: ['hello'], scope: 'projects' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq({ 'count' => '0' })
get :count, params: { search: { nested: 'hello' }, scope: 'projects' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq({ 'count' => '0' })
end
it 'does NOT blow up if repository_ref contains abusive characters' do
get :count, params: {
search: 'hello',
repository_ref: "(nslookup%20hitqlwv501f.somewhere.bad%7C%7Cperl%20-e%20%22gethostbyname('hitqlwv501f.somewhere.bad')%22)",
scope: 'projects'
}
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq({ 'count' => '0' })
end
end end
describe 'GET #autocomplete' do describe 'GET #autocomplete' do
it_behaves_like 'when the user cannot read cross project', :autocomplete, { term: 'hello' } it_behaves_like 'when the user cannot read cross project', :autocomplete, { term: 'hello' }
it_behaves_like 'with external authorization service enabled', :autocomplete, { term: 'hello' } it_behaves_like 'with external authorization service enabled', :autocomplete, { term: 'hello' }
it_behaves_like 'support for active record query timeouts', :autocomplete, { term: 'hello' }, :project, :json it_behaves_like 'support for active record query timeouts', :autocomplete, { term: 'hello' }, :project, :json
it 'returns an empty array when given abusive search term' do
get :autocomplete, params: { term: ('hal' * 9000), scope: 'projects' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to match_array([])
end
end end
describe '#append_info_to_payload' do describe '#append_info_to_payload' do
@ -358,6 +387,35 @@ RSpec.describe SearchController do
get :show, params: { search: 'hello world', group_id: '123', project_id: '456' } get :show, params: { search: 'hello world', group_id: '123', project_id: '456' }
end end
end end
context 'abusive searches', :aggregate_failures do
let(:project) { create(:project, :public, name: 'hello world') }
let(:make_abusive_request) do
get :show, params: { scope: '1;drop%20tables;boom', search: 'hello world', project_id: project.id }
end
before do
enable_external_authorization_service_check
end
it 'returns EmptySearchResults' do
expect(Gitlab::EmptySearchResults).to receive(:new).and_call_original
make_abusive_request
expect(response).to have_gitlab_http_status(:ok)
end
context 'when the feature flag is disabled' do
before do
stub_feature_flags(prevent_abusive_searches: false)
end
it 'returns a regular search result' do
expect(Gitlab::EmptySearchResults).not_to receive(:new)
make_abusive_request
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end end
context 'unauthorized user' do context 'unauthorized user' do

View File

@ -12,7 +12,7 @@ RSpec.describe 'Issue board filters', :js do
let_it_be(:release) { create(:release, tag: 'v1.0', project: project, milestones: [milestone_1]) } let_it_be(:release) { create(:release, tag: 'v1.0', project: project, milestones: [milestone_1]) }
let_it_be(:release_2) { create(:release, tag: 'v2.0', project: project, milestones: [milestone_2]) } let_it_be(:release_2) { create(:release, tag: 'v2.0', project: project, milestones: [milestone_2]) }
let_it_be(:issue_1) { create(:issue, project: project, milestone: milestone_1, author: user) } let_it_be(:issue_1) { create(:issue, project: project, milestone: milestone_1, author: user) }
let_it_be(:issue_2) { create(:labeled_issue, project: project, milestone: milestone_2, assignees: [user], labels: [project_label]) } let_it_be(:issue_2) { create(:labeled_issue, project: project, milestone: milestone_2, assignees: [user], labels: [project_label], confidential: true) }
let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: issue_1) } let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: issue_1) }
let(:filtered_search) { find('[data-testid="issue_1-board-filtered-search"]') } let(:filtered_search) { find('[data-testid="issue_1-board-filtered-search"]') }
@ -100,6 +100,25 @@ RSpec.describe 'Issue board filters', :js do
end end
end end
describe 'filters by confidentiality' do
before do
filter_input.click
filter_input.set("confidential:")
end
it 'loads all the confidentiality options when opened and submit one as filter', :aggregate_failures do
expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 2)
expect_filtered_search_dropdown_results(filter_dropdown, 2)
filter_dropdown.click_on 'Yes'
filter_submit.click
expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 1)
expect(find('.board-card')).to have_content(issue_2.title)
end
end
describe 'filters by milestone' do describe 'filters by milestone' do
before do before do
set_filter('milestone') set_filter('milestone')

View File

@ -29,6 +29,24 @@ RSpec.describe 'User visits their profile' do
expect(find('.file-content')).to have_content('testme') expect(find('.file-content')).to have_content('testme')
end end
it 'hides empty user readme' do
project = create(:project, :repository, :public, path: user.username, namespace: user.namespace)
Files::UpdateService.new(
project,
user,
start_branch: 'master',
branch_name: 'master',
commit_message: 'Update feature',
file_path: 'README.md',
file_content: ''
).execute
visit(user_path(user))
expect(page).not_to have_selector('.file-content')
end
context 'when user has groups' do context 'when user has groups' do
let(:group) do let(:group) do
create :group do |group| create :group do |group|

View File

@ -552,7 +552,20 @@ export const mockEmojiToken = {
fetchEmojis: expect.any(Function), fetchEmojis: expect.any(Function),
}; };
export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, hasEmoji) => [ export const mockConfidentialToken = {
type: 'confidential',
icon: 'eye-slash',
title: 'Confidential',
unique: true,
token: GlFilteredSearchToken,
operators: [{ value: '=', description: 'is' }],
options: [
{ icon: 'eye-slash', value: 'yes', title: 'Yes' },
{ icon: 'eye', value: 'no', title: 'No' },
],
};
export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedIn) => [
{ {
icon: 'user', icon: 'user',
title: __('Assignee'), title: __('Assignee'),
@ -593,7 +606,7 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, hasEmoji)
symbol: '~', symbol: '~',
fetchLabels, fetchLabels,
}, },
...(hasEmoji ? [mockEmojiToken] : []), ...(isSignedIn ? [mockEmojiToken, mockConfidentialToken] : []),
{ {
icon: 'clock', icon: 'clock',
title: __('Milestone'), title: __('Milestone'),

View File

@ -13,7 +13,6 @@ Array [
"id": "6", "id": "6",
"name": "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl", "name": "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
"needs": Array [], "needs": Array [],
"previousStageJobsOrNeeds": Array [],
"scheduledAt": null, "scheduledAt": null,
"status": Object { "status": Object {
"__typename": "DetailedStatus", "__typename": "DetailedStatus",
@ -54,7 +53,6 @@ Array [
"id": "11", "id": "11",
"name": "build_b", "name": "build_b",
"needs": Array [], "needs": Array [],
"previousStageJobsOrNeeds": Array [],
"scheduledAt": null, "scheduledAt": null,
"status": Object { "status": Object {
"__typename": "DetailedStatus", "__typename": "DetailedStatus",
@ -95,7 +93,6 @@ Array [
"id": "16", "id": "16",
"name": "build_c", "name": "build_c",
"needs": Array [], "needs": Array [],
"previousStageJobsOrNeeds": Array [],
"scheduledAt": null, "scheduledAt": null,
"status": Object { "status": Object {
"__typename": "DetailedStatus", "__typename": "DetailedStatus",
@ -136,7 +133,6 @@ Array [
"id": "21", "id": "21",
"name": "build_d 1/3", "name": "build_d 1/3",
"needs": Array [], "needs": Array [],
"previousStageJobsOrNeeds": Array [],
"scheduledAt": null, "scheduledAt": null,
"status": Object { "status": Object {
"__typename": "DetailedStatus", "__typename": "DetailedStatus",
@ -161,7 +157,6 @@ Array [
"id": "24", "id": "24",
"name": "build_d 2/3", "name": "build_d 2/3",
"needs": Array [], "needs": Array [],
"previousStageJobsOrNeeds": Array [],
"scheduledAt": null, "scheduledAt": null,
"status": Object { "status": Object {
"__typename": "DetailedStatus", "__typename": "DetailedStatus",
@ -186,7 +181,6 @@ Array [
"id": "27", "id": "27",
"name": "build_d 3/3", "name": "build_d 3/3",
"needs": Array [], "needs": Array [],
"previousStageJobsOrNeeds": Array [],
"scheduledAt": null, "scheduledAt": null,
"status": Object { "status": Object {
"__typename": "DetailedStatus", "__typename": "DetailedStatus",
@ -227,7 +221,6 @@ Array [
"id": "59", "id": "59",
"name": "test_c", "name": "test_c",
"needs": Array [], "needs": Array [],
"previousStageJobsOrNeeds": Array [],
"scheduledAt": null, "scheduledAt": null,
"status": Object { "status": Object {
"__typename": "DetailedStatus", "__typename": "DetailedStatus",
@ -274,11 +267,6 @@ Array [
"build_b", "build_b",
"build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl", "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
], ],
"previousStageJobsOrNeeds": Array [
"build_c",
"build_b",
"build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
],
"scheduledAt": null, "scheduledAt": null,
"status": Object { "status": Object {
"__typename": "DetailedStatus", "__typename": "DetailedStatus",
@ -325,13 +313,6 @@ Array [
"build_b", "build_b",
"build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl", "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
], ],
"previousStageJobsOrNeeds": Array [
"build_d 3/3",
"build_d 2/3",
"build_d 1/3",
"build_b",
"build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
],
"scheduledAt": null, "scheduledAt": null,
"status": Object { "status": Object {
"__typename": "DetailedStatus", "__typename": "DetailedStatus",
@ -362,13 +343,6 @@ Array [
"build_b", "build_b",
"build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl", "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
], ],
"previousStageJobsOrNeeds": Array [
"build_d 3/3",
"build_d 2/3",
"build_d 1/3",
"build_b",
"build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
],
"scheduledAt": null, "scheduledAt": null,
"status": Object { "status": Object {
"__typename": "DetailedStatus", "__typename": "DetailedStatus",
@ -411,9 +385,6 @@ Array [
"needs": Array [ "needs": Array [
"build_b", "build_b",
], ],
"previousStageJobsOrNeeds": Array [
"build_b",
],
"scheduledAt": null, "scheduledAt": null,
"status": Object { "status": Object {
"__typename": "DetailedStatus", "__typename": "DetailedStatus",

View File

@ -73,10 +73,6 @@ export const mockPipelineResponse = {
__typename: 'CiBuildNeedConnection', __typename: 'CiBuildNeedConnection',
nodes: [], nodes: [],
}, },
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [],
},
}, },
], ],
}, },
@ -122,10 +118,6 @@ export const mockPipelineResponse = {
__typename: 'CiBuildNeedConnection', __typename: 'CiBuildNeedConnection',
nodes: [], nodes: [],
}, },
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [],
},
}, },
], ],
}, },
@ -171,10 +163,6 @@ export const mockPipelineResponse = {
__typename: 'CiBuildNeedConnection', __typename: 'CiBuildNeedConnection',
nodes: [], nodes: [],
}, },
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [],
},
}, },
], ],
}, },
@ -220,10 +208,6 @@ export const mockPipelineResponse = {
__typename: 'CiBuildNeedConnection', __typename: 'CiBuildNeedConnection',
nodes: [], nodes: [],
}, },
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [],
},
}, },
{ {
__typename: 'CiJob', __typename: 'CiJob',
@ -251,10 +235,6 @@ export const mockPipelineResponse = {
__typename: 'CiBuildNeedConnection', __typename: 'CiBuildNeedConnection',
nodes: [], nodes: [],
}, },
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [],
},
}, },
{ {
__typename: 'CiJob', __typename: 'CiJob',
@ -282,10 +262,6 @@ export const mockPipelineResponse = {
__typename: 'CiBuildNeedConnection', __typename: 'CiBuildNeedConnection',
nodes: [], nodes: [],
}, },
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [],
},
}, },
], ],
}, },
@ -363,27 +339,6 @@ export const mockPipelineResponse = {
}, },
], ],
}, },
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [
{
__typename: 'CiBuildNeed',
id: '37',
name: 'build_c',
},
{
__typename: 'CiBuildNeed',
id: '38',
name: 'build_b',
},
{
__typename: 'CiBuildNeed',
id: '39',
name:
'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
},
],
},
}, },
], ],
}, },
@ -456,37 +411,6 @@ export const mockPipelineResponse = {
}, },
], ],
}, },
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [
{
__typename: 'CiBuildNeed',
id: '45',
name: 'build_d 3/3',
},
{
__typename: 'CiBuildNeed',
id: '46',
name: 'build_d 2/3',
},
{
__typename: 'CiBuildNeed',
id: '47',
name: 'build_d 1/3',
},
{
__typename: 'CiBuildNeed',
id: '48',
name: 'build_b',
},
{
__typename: 'CiBuildNeed',
id: '49',
name:
'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
},
],
},
}, },
{ {
__typename: 'CiJob', __typename: 'CiJob',
@ -541,37 +465,6 @@ export const mockPipelineResponse = {
}, },
], ],
}, },
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [
{
__typename: 'CiBuildNeed',
id: '52',
name: 'build_d 3/3',
},
{
__typename: 'CiBuildNeed',
id: '53',
name: 'build_d 2/3',
},
{
__typename: 'CiBuildNeed',
id: '54',
name: 'build_d 1/3',
},
{
__typename: 'CiBuildNeed',
id: '55',
name: 'build_b',
},
{
__typename: 'CiBuildNeed',
id: '56',
name:
'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
},
],
},
}, },
], ],
}, },
@ -610,10 +503,6 @@ export const mockPipelineResponse = {
__typename: 'CiBuildNeedConnection', __typename: 'CiBuildNeedConnection',
nodes: [], nodes: [],
}, },
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [],
},
}, },
], ],
}, },
@ -658,16 +547,6 @@ export const mockPipelineResponse = {
}, },
], ],
}, },
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [
{
__typename: 'CiBuildNeed',
id: '65',
name: 'build_b',
},
],
},
}, },
], ],
}, },
@ -841,10 +720,6 @@ export const wrappedPipelineReturn = {
__typename: 'CiBuildNeedConnection', __typename: 'CiBuildNeedConnection',
nodes: [], nodes: [],
}, },
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [],
},
status: { status: {
__typename: 'DetailedStatus', __typename: 'DetailedStatus',
id: '84', id: '84',

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::EmptySearchResults do
subject { described_class.new }
describe '#objects' do
it 'returns an empty array' do
expect(subject.objects).to match_array([])
end
end
describe '#formatted_count' do
it 'returns a zero' do
expect(subject.formatted_count).to eq('0')
end
end
describe '#highlight_map' do
it 'returns an empty hash' do
expect(subject.highlight_map).to eq({})
end
end
describe '#aggregations' do
it 'returns an empty array' do
expect(subject.objects).to match_array([])
end
end
end

View File

@ -0,0 +1,114 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Search::AbuseDetection do
subject { described_class.new(params) }
let(:params) {{ query_string: 'foobar' }}
describe 'abusive scopes validation' do
it 'allows only approved scopes' do
described_class::ALLOWED_SCOPES.each do |scope|
expect(described_class.new(scope: scope)).to be_valid
end
end
it 'disallows anything not approved' do
expect(described_class.new(scope: 'nope')).not_to be_valid
end
end
describe 'abusive character matching' do
refs = %w(
main
тест
maiñ
main123
main-v123
main-v12.3
feature/it_works
really_important!
)
refs.each do |ref|
it "does match refs permitted by git refname: #{ref}" do
[:repository_ref, :project_ref].each do |param|
validation = described_class.new(Hash[param, ref])
expect(validation).to be_valid
end
end
it "does NOT match refs with special characters: #{ref}" do
['?', '\\', ' '].each do |special_character|
[:repository_ref, :project_ref].each do |param|
validation = described_class.new(Hash[param, ref + special_character])
expect(validation).not_to be_valid
end
end
end
end
end
describe 'numericality validation' do
it 'considers non Integers to be invalid' do
[:project_id, :group_id].each do |param|
[[1, 2, 3], 'xyz', 3.14, { foo: :bar }].each do |dtype|
expect(described_class.new(param => dtype)).not_to be_valid
end
end
end
it 'considers Integers to be valid' do
[:project_id, :group_id].each do |param|
expect(described_class.new(param => 123)).to be_valid
end
end
end
describe 'query_string validation' do
using ::RSpec::Parameterized::TableSyntax
subject { described_class.new(query_string: search) }
let(:validation_errors) do
subject.validate
subject.errors.messages
end
where(:search, :errors) do
described_class::STOP_WORDS.each do |word|
word | { query_string: ['stopword only abusive search detected'] }
end
'x' | { query_string: ['abusive tiny search detected'] }
('x' * described_class::ABUSIVE_TERM_SIZE) | { query_string: ['abusive term length detected'] }
'' | {}
'*' | {}
'ruby' | {}
end
with_them do
it 'validates query string for pointless search' do
expect(validation_errors).to eq(errors)
end
end
end
describe 'abusive type coercion from string validation' do
it 'considers anything not a String invalid' do
[:query_string, :scope, :repository_ref, :project_ref].each do |param|
[[1, 2, 3], 123, 3.14, { foo: :bar }].each do |dtype|
expect(described_class.new(param => dtype)).not_to be_valid
end
end
end
it 'considers Strings to be valid' do
[:query_string, :repository_ref, :project_ref].each do |param|
expect(described_class.new(param => "foo")).to be_valid
end
end
end
end

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Search::AbuseValidators::NoAbusiveCoercionFromStringValidator do
subject do
described_class.new({ attributes: { foo: :bar } })
end
let(:instance) { double(:instance) }
let(:attribute) { :attribute }
let(:validation_msg) { 'abusive coercion from string detected' }
let(:validate) { subject.validate_each(instance, attribute, attribute_value) }
using ::RSpec::Parameterized::TableSyntax
where(:attribute_value, :valid?) do
['this is an arry'] | false
{ 'this': 'is a hash' } | false
123 | false
456.78 | false
'now this is a string' | true
end
with_them do
it do
if valid?
expect(instance).not_to receive(:errors)
else
expect(instance).to receive_message_chain(:errors, :add).with(attribute, validation_msg)
validate
end
end
end
end

View File

@ -0,0 +1,71 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Search::AbuseValidators::NoAbusiveTermLengthValidator do
subject do
described_class.new({ attributes: { foo: :bar }, maximum: limit, maximum_for_url: url_limit })
end
let(:limit) { 100 }
let(:url_limit) { limit * 2 }
let(:instance) { double(:instance) }
let(:attribute) { :search }
let(:validation_msg) { 'abusive term length detected' }
let(:validate) { subject.validate_each(instance, attribute, search) }
context 'when a term is over the limit' do
let(:search) { "this search is too lo#{'n' * limit}g" }
it 'adds a validation error' do
expect(instance).to receive_message_chain(:errors, :add).with(attribute, validation_msg)
validate
end
end
context 'when all terms are under the limit' do
let(:search) { "what is love? baby don't hurt me" }
it 'does NOT add any validation errors' do
expect(instance).not_to receive(:errors)
validate
end
end
context 'when a URL is detected in a search term' do
let(:double_limit) { limit * 2 }
let(:terms) do
[
'http://' + 'x' * (double_limit - 12) + '.com',
'https://' + 'x' * (double_limit - 13) + '.com',
'sftp://' + 'x' * (double_limit - 12) + '.com',
'ftp://' + 'x' * (double_limit - 11) + '.com',
'http://' + 'x' * (double_limit - 8) # no tld is OK
]
end
context 'when under twice the limit' do
let(:search) { terms.join(' ') }
it 'does NOT add any validation errors' do
search.split.each do |term|
expect(term.length).to be < url_limit
end
expect(instance).not_to receive(:errors)
validate
end
end
context 'when over twice the limit' do
let(:search) do
terms.map { |t| t + 'xxxxxxxx' }.join(' ')
end
it 'adds a validation error' do
expect(instance).to receive_message_chain(:errors, :add).with(attribute, validation_msg)
validate
end
end
end
end

View File

@ -0,0 +1,136 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Search::Params do
subject { described_class.new(params, detect_abuse: detect_abuse) }
let(:search) { 'search' }
let(:group_id) { 123 }
let(:params) { { group_id: 123, search: search } }
let(:detect_abuse) { true }
describe 'detect_abuse conditional' do
it 'does not call AbuseDetection' do
expect(Gitlab::Search::AbuseDetection).not_to receive(:new)
described_class.new(params, detect_abuse: false)
end
it 'uses AbuseDetection by default' do
expect(Gitlab::Search::AbuseDetection).to receive(:new).and_call_original
described_class.new(params)
end
end
describe '#[]' do
it 'feels like regular params' do
expect(subject[:group_id]).to eq(params[:group_id])
end
it 'has indifferent access' do
params = described_class.new({ 'search' => search, group_id: group_id })
expect(params['group_id']).to eq(group_id)
expect(params[:search]).to eq(search)
end
it 'also works on attr_reader attributes' do
expect(subject[:query_string]).to eq(subject.query_string)
end
end
describe '#query_string' do
let(:term) { 'term' }
it "uses 'search' parameter" do
params = described_class.new({ search: search })
expect(params.query_string).to eq(search)
end
it "uses 'term' parameter" do
params = described_class.new({ term: term })
expect(params.query_string).to eq(term)
end
it "prioritizes 'search' over 'term'" do
params = described_class.new({ search: search, term: term })
expect(params.query_string).to eq(search)
end
it 'strips surrounding whitespace from query string' do
params = described_class.new({ search: ' ' + search + ' ' })
expect(params.query_string).to eq(search)
end
end
describe '#validate' do
context 'when detect_abuse is disabled' do
let(:detect_abuse) { false }
it 'does NOT validate AbuseDetector' do
expect(Gitlab::Search::AbuseDetection).not_to receive(:new)
subject.validate
end
end
it 'validates AbuseDetector on validation' do
expect(Gitlab::Search::AbuseDetection).to receive(:new).and_call_original
subject.validate
end
end
describe '#valid?' do
context 'when detect_abuse is disabled' do
let(:detect_abuse) { false }
it 'does NOT validate AbuseDetector' do
expect(Gitlab::Search::AbuseDetection).not_to receive(:new)
subject.valid?
end
end
it 'validates AbuseDetector on validation' do
expect(Gitlab::Search::AbuseDetection).to receive(:new).and_call_original
subject.valid?
end
end
describe 'abuse detection' do
let(:abuse_detection) { instance_double(Gitlab::Search::AbuseDetection) }
before do
allow(subject).to receive(:abuse_detection).and_return abuse_detection
allow(abuse_detection).to receive(:errors).and_return abuse_errors
end
context 'when there are abuse validation errors' do
let(:abuse_errors) { { foo: ['bar'] } }
it 'is considered abusive' do
expect(subject).to be_abusive
end
end
context 'when there are NOT any abuse validation errors' do
let(:abuse_errors) { {} }
context 'and there are other validation errors' do
it 'is NOT considered abusive' do
allow(subject).to receive(:valid?) do
subject.errors.add :project_id, 'validation error unrelated to abuse'
false
end
expect(subject).not_to be_abusive
end
end
context 'and there are NO other validation errors' do
it 'is NOT considered abusive' do
allow(subject).to receive(:valid?).and_return(true)
expect(subject).not_to be_abusive
end
end
end
end
end

View File

@ -1317,10 +1317,28 @@ RSpec.describe Issue do
let_it_be(:issue1) { create(:issue, project: project, relative_position: nil) } let_it_be(:issue1) { create(:issue, project: project, relative_position: nil) }
let_it_be(:issue2) { create(:issue, project: project, relative_position: nil) } let_it_be(:issue2) { create(:issue, project: project, relative_position: nil) }
it_behaves_like "a class that supports relative positioning" do context 'when optimized_issue_neighbor_queries is enabled' do
let_it_be(:project) { reusable_project } before do
let(:factory) { :issue } stub_feature_flags(optimized_issue_neighbor_queries: true)
let(:default_params) { { project: project } } end
it_behaves_like "a class that supports relative positioning" do
let_it_be(:project) { reusable_project }
let(:factory) { :issue }
let(:default_params) { { project: project } }
end
end
context 'when optimized_issue_neighbor_queries is disabled' do
before do
stub_feature_flags(optimized_issue_neighbor_queries: false)
end
it_behaves_like "a class that supports relative positioning" do
let_it_be(:project) { reusable_project }
let(:factory) { :issue }
let(:default_params) { { project: project } }
end
end end
it 'is not blocked for repositioning by default' do it 'is not blocked for repositioning by default' do

View File

@ -87,36 +87,10 @@ module Ci
end end
context 'for specific runner' do context 'for specific runner' do
context 'with tables decoupling disabled' do it 'does not pick a build' do
before do expect(execute(specific_runner)).to be_nil
stub_feature_flags( expect(pending_job.reload).to be_failed
ci_pending_builds_project_runners_decoupling: false, expect(pending_job.queuing_entry).to be_nil
ci_queueing_builds_enabled_checks: false)
end
around do |example|
allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332952') do
example.run
end
end
it 'does not pick a build' do
expect(execute(specific_runner)).to be_nil
end
end
context 'with tables decoupling enabled' do
before do
stub_feature_flags(
ci_pending_builds_project_runners_decoupling: true,
ci_queueing_builds_enabled_checks: true)
end
it 'does not pick a build' do
expect(execute(specific_runner)).to be_nil
expect(pending_job.reload).to be_failed
expect(pending_job.queuing_entry).to be_nil
end
end end
end end
end end
@ -272,34 +246,10 @@ module Ci
context 'and uses project runner' do context 'and uses project runner' do
let(:build) { execute(specific_runner) } let(:build) { execute(specific_runner) }
context 'with tables decoupling disabled' do it 'does not pick a build' do
before do expect(build).to be_nil
stub_feature_flags( expect(pending_job.reload).to be_failed
ci_pending_builds_project_runners_decoupling: false, expect(pending_job.queuing_entry).to be_nil
ci_queueing_builds_enabled_checks: false)
end
around do |example|
allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332952') do
example.run
end
end
it { expect(build).to be_nil }
end
context 'with tables decoupling enabled' do
before do
stub_feature_flags(
ci_pending_builds_project_runners_decoupling: true,
ci_queueing_builds_enabled_checks: true)
end
it 'does not pick a build' do
expect(build).to be_nil
expect(pending_job.reload).to be_failed
expect(pending_job.queuing_entry).to be_nil
end
end end
end end
end end

View File

@ -20,6 +20,7 @@ RSpec.describe SearchService do
let(:page) { 1 } let(:page) { 1 }
let(:per_page) { described_class::DEFAULT_PER_PAGE } let(:per_page) { described_class::DEFAULT_PER_PAGE }
let(:valid_search) { "what is love?" }
subject(:search_service) { described_class.new(user, search: search, scope: scope, page: page, per_page: per_page) } subject(:search_service) { described_class.new(user, search: search, scope: scope, page: page, per_page: per_page) }
@ -30,7 +31,7 @@ RSpec.describe SearchService do
describe '#project' do describe '#project' do
context 'when the project is accessible' do context 'when the project is accessible' do
it 'returns the project' do it 'returns the project' do
project = described_class.new(user, project_id: accessible_project.id).project project = described_class.new(user, project_id: accessible_project.id, search: valid_search).project
expect(project).to eq accessible_project expect(project).to eq accessible_project
end end
@ -39,7 +40,7 @@ RSpec.describe SearchService do
search_project = create :project search_project = create :project
search_project.add_guest(user) search_project.add_guest(user)
project = described_class.new(user, project_id: search_project.id).project project = described_class.new(user, project_id: search_project.id, search: valid_search).project
expect(project).to eq search_project expect(project).to eq search_project
end end
@ -47,7 +48,7 @@ RSpec.describe SearchService do
context 'when the project is not accessible' do context 'when the project is not accessible' do
it 'returns nil' do it 'returns nil' do
project = described_class.new(user, project_id: inaccessible_project.id).project project = described_class.new(user, project_id: inaccessible_project.id, search: valid_search).project
expect(project).to be_nil expect(project).to be_nil
end end
@ -55,7 +56,7 @@ RSpec.describe SearchService do
context 'when there is no project_id' do context 'when there is no project_id' do
it 'returns nil' do it 'returns nil' do
project = described_class.new(user).project project = described_class.new(user, search: valid_search).project
expect(project).to be_nil expect(project).to be_nil
end end
@ -65,7 +66,7 @@ RSpec.describe SearchService do
describe '#group' do describe '#group' do
context 'when the group is accessible' do context 'when the group is accessible' do
it 'returns the group' do it 'returns the group' do
group = described_class.new(user, group_id: accessible_group.id).group group = described_class.new(user, group_id: accessible_group.id, search: valid_search).group
expect(group).to eq accessible_group expect(group).to eq accessible_group
end end
@ -73,7 +74,7 @@ RSpec.describe SearchService do
context 'when the group is not accessible' do context 'when the group is not accessible' do
it 'returns nil' do it 'returns nil' do
group = described_class.new(user, group_id: inaccessible_group.id).group group = described_class.new(user, group_id: inaccessible_group.id, search: valid_search).group
expect(group).to be_nil expect(group).to be_nil
end end
@ -81,7 +82,7 @@ RSpec.describe SearchService do
context 'when there is no group_id' do context 'when there is no group_id' do
it 'returns nil' do it 'returns nil' do
group = described_class.new(user).group group = described_class.new(user, search: valid_search).group
expect(group).to be_nil expect(group).to be_nil
end end
@ -118,7 +119,7 @@ RSpec.describe SearchService do
context 'with accessible project_id' do context 'with accessible project_id' do
context 'and allowed scope' do context 'and allowed scope' do
it 'returns the specified scope' do it 'returns the specified scope' do
scope = described_class.new(user, project_id: accessible_project.id, scope: 'notes').scope scope = described_class.new(user, project_id: accessible_project.id, scope: 'notes', search: valid_search).scope
expect(scope).to eq 'notes' expect(scope).to eq 'notes'
end end
@ -126,7 +127,7 @@ RSpec.describe SearchService do
context 'and disallowed scope' do context 'and disallowed scope' do
it 'returns the default scope' do it 'returns the default scope' do
scope = described_class.new(user, project_id: accessible_project.id, scope: 'projects').scope scope = described_class.new(user, project_id: accessible_project.id, scope: 'projects', search: valid_search).scope
expect(scope).to eq 'blobs' expect(scope).to eq 'blobs'
end end
@ -134,7 +135,7 @@ RSpec.describe SearchService do
context 'and no scope' do context 'and no scope' do
it 'returns the default scope' do it 'returns the default scope' do
scope = described_class.new(user, project_id: accessible_project.id).scope scope = described_class.new(user, project_id: accessible_project.id, search: valid_search).scope
expect(scope).to eq 'blobs' expect(scope).to eq 'blobs'
end end
@ -552,4 +553,66 @@ RSpec.describe SearchService do
end end
end end
end end
describe '#valid_request?' do
let(:scope) { 'issues' }
let(:search) { 'foobar' }
let(:params) { instance_double(Gitlab::Search::Params) }
before do
allow(Gitlab::Search::Params).to receive(:new).and_return(params)
allow(params).to receive(:valid?).and_return double(:valid?)
end
it 'is the return value of params.valid?' do
expect(subject.valid_request?).to eq(params.valid?)
end
end
describe 'abusive search handling' do
subject { described_class.new(user, raw_params) }
let(:raw_params) { { search: search, scope: scope } }
let(:search) { 'foobar' }
let(:search_service) { double(:search_service) }
before do
stub_feature_flags(prevent_abusive_searches: should_detect_abuse)
expect(Gitlab::Search::Params).to receive(:new)
.with(raw_params, detect_abuse: should_detect_abuse).and_call_original
allow(subject).to receive(:search_service).and_return search_service
end
context 'when abusive search but prevent_abusive_searches FF is disabled' do
let(:should_detect_abuse) { false }
let(:scope) { '1;drop%20table' }
it 'executes search even if params are abusive' do
expect(search_service).to receive(:execute)
subject.search_results
end
end
context 'a search is abusive' do
let(:should_detect_abuse) { true }
let(:scope) { '1;drop%20table' }
it 'does NOT execute search service' do
expect(search_service).not_to receive(:execute)
subject.search_results
end
end
context 'a search is NOT abusive' do
let(:should_detect_abuse) { true }
let(:scope) { 'issues' }
it 'executes search service' do
expect(search_service).to receive(:execute)
subject.search_results
end
end
end
end end