Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-03-31 00:12:17 +00:00
parent 6e12437923
commit 46f49bc8e6
97 changed files with 1397 additions and 324 deletions

View File

@ -2,6 +2,31 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 15.10.1 (2023-03-30)
### Fixed (2 changes)
- [Sync security policy rule schedules that may have been deleted by bug](gitlab-org/security/gitlab@5ac094761b5cfac26c44d63988359fbae263a415)
- [Fix issue dashboard returning issues from archived projects](gitlab-org/security/gitlab@6127799167081845824e8759f358aac8f702adb8)
### Security (15 changes)
- [Redirect to tree from project root on ref collision](gitlab-org/security/gitlab@c10a48134447128486e2254fc54d0af0d8e6fee0) ([merge request](gitlab-org/security/gitlab!3155))
- [Fixes soft email confirmation alert vulnerability](gitlab-org/security/gitlab@4aa387fec0c995607f03e8c057d2c2a11168aca9) ([merge request](gitlab-org/security/gitlab!3158))
- [Restrict Prometheus API access on public projects](gitlab-org/security/gitlab@e9cf398f8c205ae1b8cafddbb2cfbcb214a84d51) ([merge request](gitlab-org/security/gitlab!3162))
- [Verify that users have access to the parent of the fork](gitlab-org/security/gitlab@fb55096b37ab82f49f2a0205f7ab8bdda14b0010) ([merge request](gitlab-org/security/gitlab!3153))
- [Protect webhook secrets by resetting url_variables](gitlab-org/security/gitlab@433996f41e89db3e2073314c0644a6f95ab67062) ([merge request](gitlab-org/security/gitlab!3146))
- [Replace Unicode space chars with spaces](gitlab-org/security/gitlab@c9942785d9a26cf7bb96a81ccd14e5c6e5582bbe) ([merge request](gitlab-org/security/gitlab!3156))
- [Check access to parent when creating and updating epics](gitlab-org/security/gitlab@a42d166e743edb966b0a581bf1325ffb7c96041b) ([merge request](gitlab-org/security/gitlab!3148))
- [Improve Gitlab::UrlSanitizer regex to match more URIs](gitlab-org/security/gitlab@58a823e09c27948d15432c344248a8436587f9af) ([merge request](gitlab-org/security/gitlab!3165))
- [Check access to target project before looking for branch](gitlab-org/security/gitlab@804d9da677451889e0a7a0880f2c2f4c3c04faed) ([merge request](gitlab-org/security/gitlab!3151))
- [Fix the potential leak of internal notes](gitlab-org/security/gitlab@e21dbf4373a4c4e5179b073f5cba4318ee174918) ([merge request](gitlab-org/security/gitlab!3154))
- [Use UntrustedRegexp to limit scan of HTML comments](gitlab-org/security/gitlab@874edf184764fa801866fbd4e89b9f7e87c570fd) ([merge request](gitlab-org/security/gitlab!3143))
- [Filter namespace environments by feature visibility](gitlab-org/security/gitlab@e88f78f19dc5ed01a74e6c0d4bb5c22f3a69b65b) ([merge request](gitlab-org/security/gitlab!3114))
- [Check access to reorder issues in epic tree](gitlab-org/security/gitlab@94e4e543762998a9bbff75c5ffb5cd5da6bd2d88) ([merge request](gitlab-org/security/gitlab!3147))
- [Fix security report authorization](gitlab-org/security/gitlab@10f33b260212ebf811acecf4b05af1311b44fb64) ([merge request](gitlab-org/security/gitlab!3145))
- [Prevent XSS attack in "Maximum page reached" page](gitlab-org/security/gitlab@4ce175e4096c973a2d16b93fff6b60bc0144eee0) ([merge request](gitlab-org/security/gitlab!3132))
## 15.10.0 (2023-03-21)
### Added (155 changes)
@ -726,6 +751,27 @@ entry.
- [Update submit buttons to use Pajamas component](gitlab-org/gitlab@4ffb92755e6be3268c78f02e471f5c2a21f437be) ([merge request](gitlab-org/gitlab!114246))
## 15.9.4 (2023-03-30)
### Security (16 changes)
- [Add checks to remove open redirects from Observability URL](gitlab-org/security/gitlab@98b1bd243f454bd28c262131be616ee2060c3a78) ([merge request](gitlab-org/security/gitlab!3104))
- [Redirect to tree from project root on ref collision](gitlab-org/security/gitlab@0f0c0f21dffe300a56abf1e07a2fefb17160faeb) ([merge request](gitlab-org/security/gitlab!3133))
- [Fixes soft email confirmation alert vulnerability](gitlab-org/security/gitlab@12498f791f9c5fe833f5202b06cc818d4dcf965b) ([merge request](gitlab-org/security/gitlab!3124))
- [Restrict Prometheus API access on public projects](gitlab-org/security/gitlab@440a7989ff46ca333f86a38aefa47f74301e66fc) ([merge request](gitlab-org/security/gitlab!3163))
- [Verify that users have access to the parent of the fork](gitlab-org/security/gitlab@9dd0dff69d3941e827c461c67b9af10da07d69f8) ([merge request](gitlab-org/security/gitlab!3084))
- [Protect webhook secrets by resetting url_variables](gitlab-org/security/gitlab@cd20b44dd5b075827203330802e331b896448265) ([merge request](gitlab-org/security/gitlab!3140))
- [Replace Unicode space chars with spaces](gitlab-org/security/gitlab@76975082c41870265e1285fa8f4e053eb6ff11ae) ([merge request](gitlab-org/security/gitlab!3136))
- [Check access to parent when creating and updating epics](gitlab-org/security/gitlab@7fcc4a0d010d3a428e803f95ef47904c4c7178a8) ([merge request](gitlab-org/security/gitlab!3149))
- [Improve Gitlab::UrlSanitizer regex to match more URIs](gitlab-org/security/gitlab@4e7313536e4cdb3ecef37100b5a73720eabfbc79) ([merge request](gitlab-org/security/gitlab!3108))
- [Check access to target project before looking for branch](gitlab-org/security/gitlab@f55edf39e52af9eecb19caf8ed5d4cb8524ef64d) ([merge request](gitlab-org/security/gitlab!3040))
- [Fix the potential leak of internal notes](gitlab-org/security/gitlab@be73600e8c43c22cda1ace5910eb2052b2741972) ([merge request](gitlab-org/security/gitlab!3120))
- [Use UntrustedRegexp to limit scan of HTML comments](gitlab-org/security/gitlab@d5e65583debcae71787e171643275bc9b9d4393e) ([merge request](gitlab-org/security/gitlab!3142))
- [Filter namespace environments by feature visibility](gitlab-org/security/gitlab@54045b508a9ba9ae18f5992b77970240774b28a7) ([merge request](gitlab-org/security/gitlab!3111))
- [Check access to reorder issues in epic tree](gitlab-org/security/gitlab@bc033cd3a98c9a1468545811a8180604f7f8aee3) ([merge request](gitlab-org/security/gitlab!3101))
- [Fix security report authorization](gitlab-org/security/gitlab@a01cf9d8383ffc4c0e29514f71d49bf345e1f7c2) ([merge request](gitlab-org/security/gitlab!3106))
- [Prevent XSS attack in "Maximum page reached" page](gitlab-org/security/gitlab@3cefb16a5e369ee99f4c3ccbaa02cead6faf1a99) ([merge request](gitlab-org/security/gitlab!3130))
## 15.9.3 (2023-03-09)
### Fixed (4 changes)
@ -1482,6 +1528,27 @@ entry.
- [Remove Gitlab::Redis::DuplicateJobs](gitlab-org/gitlab@73d863b0a49175cce7649c0936b2e16157f61665) ([merge request](gitlab-org/gitlab!109122))
- [Clean-up feature flag `hash_based_cache_for_protected_branches`](gitlab-org/gitlab@96e8a07564bac07a100556e00ce4af3f21dca293) ([merge request](gitlab-org/gitlab!108724))
## 15.8.5 (2023-03-30)
### Security (16 changes)
- [Fix rubocop offenses in lib/gitlab/url_sanitizer.rb](gitlab-org/security/gitlab@ddc04cf7059e411e20033b95e1297381d64d4b22) ([merge request](gitlab-org/security/gitlab!3175))
- [Add checks to remove open redirects from Observability URL](gitlab-org/security/gitlab@a22ce3851128eb900dbabe9e38c07889967a2915) ([merge request](gitlab-org/security/gitlab!3032))
- [Redirect to tree from project root on ref collision](gitlab-org/security/gitlab@fad24ae9d8fa0e7bd9eff0c9e6914c8267451b4d) ([merge request](gitlab-org/security/gitlab!3134))
- [Fixes soft email confirmation alert vulnerability](gitlab-org/security/gitlab@85be0fbfc98cdb774d68070479e35be22f6ba40a) ([merge request](gitlab-org/security/gitlab!3125))
- [Restrict Prometheus API access on public projects](gitlab-org/security/gitlab@2df2fa2dc4b9015d044d0ddc5d26e17e9e5f85c0) ([merge request](gitlab-org/security/gitlab!3164))
- [Verify that users have access to the parent of the fork](gitlab-org/security/gitlab@53f7f06843eea4d666d361f5a1d349bd1e3f4312) ([merge request](gitlab-org/security/gitlab!3085))
- [Protect webhook secrets by resetting url_variables](gitlab-org/security/gitlab@9fa9dbff463f6015ffaf8d082db3d41ae623763e) ([merge request](gitlab-org/security/gitlab!3141))
- [Replace Unicode space chars with spaces](gitlab-org/security/gitlab@20d77d4d680d13f916fb69de0d79802753421c8f) ([merge request](gitlab-org/security/gitlab!3137))
- [Check access to parent when creating and updating epics](gitlab-org/security/gitlab@0fed113756b27a3a078f87f29711b225e1ed4cce) ([merge request](gitlab-org/security/gitlab!3150))
- [Improve Gitlab::UrlSanitizer regex to match more URIs](gitlab-org/security/gitlab@2285088f37aca877b1dcd59c728cdf33171b30cb) ([merge request](gitlab-org/security/gitlab!3109))
- [Check access to target project before looking for branch](gitlab-org/security/gitlab@37b8d855d87c88170322e6a6d4c285fee6c6cb64) ([merge request](gitlab-org/security/gitlab!3038))
- [Fix the potential leak of internal notes](gitlab-org/security/gitlab@66f8cc2eb13509397b980d53a4b67ca03d8903f7) ([merge request](gitlab-org/security/gitlab!3121))
- [Filter namespace environments by feature visibility](gitlab-org/security/gitlab@e1859de393b4794e1356d6318e56ede4b557c059) ([merge request](gitlab-org/security/gitlab!3112))
- [Check access to reorder issues in epic tree](gitlab-org/security/gitlab@13f9c6231cea956f73355c5b5b820163f523e7d8) ([merge request](gitlab-org/security/gitlab!3100))
- [Fix security report authorization](gitlab-org/security/gitlab@19baab85c7a5a64a09e3e4808e8550fc72e18323) ([merge request](gitlab-org/security/gitlab!3105))
- [Prevent XSS attack in "Maximum page reached" page](gitlab-org/security/gitlab@be5491c5db05161e4b14d53900dd19b66848de48) ([merge request](gitlab-org/security/gitlab!3131))
## 15.8.4 (2023-03-02)
### Security (12 changes)

View File

@ -517,7 +517,6 @@ export default {
ref="deleteCiVariable"
variant="danger"
category="secondary"
data-qa-selector="ci_variable_delete_button"
@click="deleteVarAndClose"
>{{ __('Delete variable') }}</gl-button
>

View File

@ -175,12 +175,7 @@ export default {
v-if="glFeatures.ciVariablesPages"
class="ci-variable-actions gl-display-flex gl-justify-content-end gl-my-3"
>
<gl-button
v-if="!isTableEmpty"
data-qa-selector="reveal_ci_variable_value_button"
@click="toggleHiddenState"
>{{ valuesButtonText }}</gl-button
>
<gl-button v-if="!isTableEmpty" @click="toggleHiddenState">{{ valuesButtonText }}</gl-button>
<gl-button
v-gl-modal-directive="$options.modalId"
class="gl-mx-3"
@ -317,12 +312,7 @@ export default {
@click="setSelectedVariable()"
>{{ __('Add variable') }}</gl-button
>
<gl-button
v-if="!isTableEmpty"
data-qa-selector="reveal_ci_variable_value_button"
@click="toggleHiddenState"
>{{ valuesButtonText }}</gl-button
>
<gl-button v-if="!isTableEmpty" @click="toggleHiddenState">{{ valuesButtonText }}</gl-button>
</div>
<div v-else class="gl-display-flex gl-justify-content-center gl-mt-6">
<gl-keyset-pagination

View File

@ -4,6 +4,9 @@ export const GENERIC_ERROR = __('Something went wrong on our end. Please try aga
export const LOAD_SINGLE_DIFF_FAILED = s__(
'MergeRequest|Encountered an issue while trying to fetch the single file diff.',
);
export const DISCUSSION_SINGLE_DIFF_FAILED = s__(
"MergeRequest|Can't fetch the single file diff for the discussion. Please reload this page.",
);
export const DIFF_FILE_HEADER = {
optionsDropdownTitle: __('Options'),

View File

@ -50,7 +50,7 @@ import {
TRACKING_SINGLE_FILE_MODE,
TRACKING_MULTIPLE_FILES_MODE,
} from '../constants';
import { LOAD_SINGLE_DIFF_FAILED } from '../i18n';
import { DISCUSSION_SINGLE_DIFF_FAILED, LOAD_SINGLE_DIFF_FAILED } from '../i18n';
import eventHub from '../event_hub';
import { isCollapsed } from '../utils/diff_file';
import { markFileReview, setReviewsForMergeRequest } from '../utils/file_reviews';
@ -896,6 +896,24 @@ export function moveToNeighboringCommit({ dispatch, state }, { direction }) {
}
}
export const rereadNoteHash = ({ state, dispatch }) => {
const urlHash = window?.location?.hash;
if (isUrlHashNoteLink(urlHash)) {
dispatch('setCurrentDiffFileIdFromNote', urlHash.split('_').pop())
.then(() => {
if (state.viewDiffsFileByFile) {
dispatch('fetchFileByFile');
}
})
.catch(() => {
createAlert({
message: DISCUSSION_SINGLE_DIFF_FAILED,
});
});
}
};
export const setCurrentDiffFileIdFromNote = ({ commit, getters, rootGetters }, noteId) => {
const note = rootGetters.notesById[noteId];

View File

@ -15,7 +15,7 @@ import '~/sourcegraph/load';
import createStore from '~/code_navigation/store';
import { generateRefDestinationPath } from '~/repository/utils/ref_switcher_utils';
import RefSelector from '~/ref/components/ref_selector.vue';
import { visitUrl } from '~/lib/utils/url_utility';
import { joinPaths, visitUrl } from '~/lib/utils/url_utility';
Vue.use(Vuex);
Vue.use(VueApollo);
@ -34,7 +34,7 @@ const initRefSwitcher = () => {
if (!refSwitcherEl) return false;
const { projectId, projectRootPath, ref } = refSwitcherEl.dataset;
const { projectId, projectRootPath, ref, refType } = refSwitcherEl.dataset;
return new Vue({
el: refSwitcherEl,
@ -42,7 +42,8 @@ const initRefSwitcher = () => {
return createElement(RefSelector, {
props: {
projectId,
value: ref,
value: refType ? joinPaths('refs', refType, ref) : ref,
useSymbolicRefNames: true,
queryParams: { sort: 'updated_desc' },
},
on: {

View File

@ -2,7 +2,7 @@ import { GlButton } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils';
import { escapeFileUrl, visitUrl } from '~/lib/utils/url_utility';
import { joinPaths, escapeFileUrl, visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import PerformancePlugin from '~/performance/vue_performance_plugin';
@ -128,7 +128,7 @@ export default function setupVueRepositoryList() {
if (!refSwitcherEl) return false;
const { projectId, projectRootPath } = refSwitcherEl.dataset;
const { projectId, projectRootPath, refType } = refSwitcherEl.dataset;
return new Vue({
el: refSwitcherEl,
@ -136,7 +136,8 @@ export default function setupVueRepositoryList() {
return createElement(RefSelector, {
props: {
projectId,
value: ref,
value: refType ? joinPaths('refs', refType, ref) : ref,
useSymbolicRefNames: true,
queryParams: { sort: 'updated_desc' },
},
on: {

View File

@ -16,22 +16,29 @@ const getNamespaceTargetRegex = (ref) => new RegExp(`(/-/(blob|tree))/${ref}/(.*
* @param {string} selectedRef - The selected ref from the ref dropdown.
*/
export function generateRefDestinationPath(projectRootPath, ref, selectedRef) {
const currentPath = window.location.pathname;
const encodedHash = '%23';
const url = new URL(window.location.href);
const currentPath = url.pathname;
let refType = null;
let namespace = '/-/tree';
let target;
let actualRef = selectedRef;
const matches = selectedRef.match(/^refs\/(heads|tags)\/(.+)/);
if (matches) {
[, refType, actualRef] = matches;
}
if (refType) {
url.searchParams.set('ref_type', refType);
} else {
url.searchParams.delete('ref_type');
}
const NAMESPACE_TARGET_REGEX = getNamespaceTargetRegex(ref);
const match = NAMESPACE_TARGET_REGEX.exec(currentPath);
if (match) {
[, namespace, , target] = match;
}
url.pathname = joinPaths(projectRootPath, namespace, actualRef, target);
const destinationPath = joinPaths(
projectRootPath,
namespace,
encodeURI(selectedRef).replace(/#/g, encodedHash),
target,
);
return `${destinationPath}${window.location.hash}`;
return url.toString();
}

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true
module ConfirmEmailWarning
include Gitlab::Utils::StrongMemoize
extend ActiveSupport::Concern
included do
@ -17,11 +18,9 @@ module ConfirmEmailWarning
return unless current_user
return if current_user.confirmed?
email = current_user.unconfirmed_email || current_user.email
flash.now[:warning] = format(
confirm_warning_message,
email: email,
email: email_to_display,
resend_link: view_context.link_to(_('Resend it'), user_confirmation_path(user: { email: email }), method: :post),
update_link: view_context.link_to(_('Update it'), profile_path)
).html_safe
@ -29,7 +28,16 @@ module ConfirmEmailWarning
private
def email
current_user.unconfirmed_email || current_user.email
end
strong_memoize_attr :email
def confirm_warning_message
_("Please check your email (%{email}) to verify that you own this address and unlock the power of CI/CD. Didn't receive it? %{resend_link}. Wrong email address? %{update_link}.")
end
def email_to_display
html_escape(email)
end
end

View File

@ -31,6 +31,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action :authorize_edit_tree!, only: [:new, :create, :update, :destroy]
before_action :commit, except: [:new, :create]
before_action :check_for_ambiguous_ref, only: [:show]
before_action :blob, except: [:new, :create]
before_action :require_branch_head, only: [:edit, :update]
before_action :editor_variables, except: [:show, :preview, :diff]
@ -156,6 +157,15 @@ class Projects::BlobController < Projects::ApplicationController
end
end
def check_for_ambiguous_ref
@ref_type = ref_type
if @ref_type == ExtractsRef::BRANCH_REF_TYPE && ambiguous_ref?(@project, @ref)
branch = @project.repository.find_branch(@ref)
redirect_to project_blob_path(@project, File.join(branch.target, @path))
end
end
def commit
@commit ||= @repository.commit(@ref)

View File

@ -27,6 +27,15 @@ class Projects::TreeController < Projects::ApplicationController
def show
return render_404 unless @commit
@ref_type = ref_type
if @ref_type == BRANCH_REF_TYPE && ambiguous_ref?(@project, @ref)
branch = @project.repository.find_branch(@ref)
if branch
redirect_to project_tree_path(@project, branch.target)
return
end
end
if tree.entries.empty?
if @repository.blob_at(@commit.id, @path)
redirect_to project_blob_path(@project, File.join(@ref, @path))

View File

@ -172,11 +172,19 @@ class ProjectsController < Projects::ApplicationController
flash.now[:alert] = _("Project '%{project_name}' queued for deletion.") % { project_name: @project.name }
end
if ambiguous_ref?(@project, @ref)
branch = @project.repository.find_branch(@ref)
# The files view would render a ref other than the default branch
# This redirect can be removed once the view is fixed
redirect_to(project_tree_path(@project, branch.target), alert: _("The default branch of this project clashes with another ref"))
return
end
respond_to do |format|
format.html do
@notification_setting = current_user.notification_settings_for(@project) if current_user
@project = @project.present(current_user: current_user)
render_landing_page
end

View File

@ -32,18 +32,9 @@ module Environments
end
def namespace_environments
# We assume reporter access is needed for the :read_environment permission
# here. This expection is also present in
# IssuableFinder::Params#min_access_level, which is used for filtering out
# merge requests that don't have the right permissions.
#
# We use this approach so we don't need to load every project into memory
# just to verify if we can see their environments. Doing so would not be
# efficient, and possibly mess up pagination if certain projects are not
# meant to be visible.
projects = project_or_group
.all_projects
.public_or_visible_to_user(current_user, Gitlab::Access::REPORTER)
.filter_by_feature_visibility(:environments, current_user)
Environment.for_project(projects)
end

View File

@ -30,6 +30,7 @@ class NotesFinder
notes = init_collection
notes = since_fetch_at(notes)
notes = notes.with_notes_filter(@params[:notes_filter]) if notes_filter?
notes = redact_internal(notes)
sort(notes)
end
@ -181,6 +182,13 @@ class NotesFinder
notes.order_by(sort)
end
def redact_internal(notes)
subject = @project || target
return notes if Ability.allowed?(@current_user, :read_internal_note, subject)
notes.not_internal
end
end
NotesFinder.prepend_mod_with('NotesFinder')

View File

@ -41,7 +41,7 @@ module DashboardHelper
if doc_href.present?
link_to_doc = link_to(
sprite_icon('question'),
sprite_icon('question-o'),
doc_href,
class: 'gl-ml-2',
title: _('Documentation'),

View File

@ -27,7 +27,14 @@ module IssueAvailableFeatures
raise ArgumentError, 'invalid feature'
end
self.class.available_features_for_issue_types[feature].include?(issue_type)
type_for_issue = if Feature.enabled?(:issue_type_uses_work_item_types_table)
# The default will only be used in places where an issue is only build and not saved
work_item_type_with_default.base_type
else
issue_type
end
self.class.available_features_for_issue_types[feature].include?(type_for_issue)
end
end

View File

@ -24,25 +24,37 @@ module Taskable
(\s.+) # followed by whitespace and some text.
}x.freeze
ITEM_PATTERN_UNTRUSTED =
'^' \
'(?:(?:>\s{0,4})*)' \
'(?P<prefix>(?:\s*(?:[-+*]|(?:\d+\.)))+)' \
'\s+' \
'(?P<checkbox>' \
"#{COMPLETE_PATTERN.source}|#{INCOMPLETE_PATTERN.source}" \
')' \
'(?P<label>\s.+)'.freeze
# ignore tasks in code or html comment blocks. HTML blocks
# are ok as we allow tasks inside <detail> blocks
REGEX = %r{
#{::Gitlab::Regex.markdown_code_or_html_comments}
|
(?<task_item>
#{ITEM_PATTERN}
)
}mx.freeze
REGEX =
"#{::Gitlab::Regex.markdown_code_or_html_comments_untrusted}" \
"|" \
"(?P<task_item>" \
"#{ITEM_PATTERN_UNTRUSTED}" \
")".freeze
def self.get_tasks(content)
items = []
content.to_s.scan(REGEX) do
next unless $~[:task_item]
regex = Gitlab::UntrustedRegexp.new(REGEX, multiline: true)
regex.scan(content.to_s).each do |match|
next unless regex.extract_named_group(:task_item, match)
$~[:task_item].scan(ITEM_PATTERN) do |prefix, checkbox, label|
items << TaskList::Item.new("#{prefix.strip} #{checkbox}", label.strip)
end
prefix = regex.extract_named_group(:prefix, match)
checkbox = regex.extract_named_group(:checkbox, match)
label = regex.extract_named_group(:label, match)
items << TaskList::Item.new("#{prefix.strip} #{checkbox}", label.strip)
end
items

View File

@ -36,7 +36,7 @@ class WebHook < ApplicationRecord
after_initialize :initialize_url_variables
before_validation :reset_token
before_validation :reset_url_variables, unless: ->(hook) { hook.is_a?(ServiceHook) }
before_validation :reset_url_variables, unless: ->(hook) { hook.is_a?(ServiceHook) }, on: :update
before_validation :set_branch_filter_nil, if: :branch_filter_strategy_all_branches?
validates :push_events_branch_filter, untrusted_regexp: true, if: :branch_filter_strategy_regex?
validates :push_events_branch_filter, "web_hooks/wildcard_branch_filter": true, if: :branch_filter_strategy_wildcard?
@ -105,7 +105,7 @@ class WebHook < ApplicationRecord
# See app/validators/json_schemas/web_hooks_url_variables.json
VARIABLE_REFERENCE_RE = /\{([A-Za-z]+[0-9]*(?:[._-][A-Za-z0-9]+)*)\}/.freeze
def interpolated_url
def interpolated_url(url = self.url, url_variables = self.url_variables)
return url unless url.include?('{')
vars = url_variables
@ -131,7 +131,19 @@ class WebHook < ApplicationRecord
end
def reset_url_variables
self.url_variables = {} if url_changed? && !encrypted_url_variables_changed?
interpolated_url_was = interpolated_url(decrypt_url_was, url_variables_were)
return if url_variables_were.empty? || interpolated_url_was == interpolated_url
self.url_variables = {} if url_changed? && url_variables_were.to_a.intersection(url_variables.to_a).any?
end
def decrypt_url_was
self.class.decrypt_url(encrypted_url_was, iv: Base64.decode64(encrypted_url_iv_was))
end
def url_variables_were
self.class.decrypt_url_variables(encrypted_url_variables_was, iv: encrypted_url_variables_iv_was)
end
def initialize_url_variables

View File

@ -52,6 +52,9 @@ class Issue < ApplicationRecord
# Types of issues that should be displayed on issue board lists
TYPES_FOR_BOARD_LIST = %w(issue incident).freeze
# This default came from the enum `issue_type` column. Defined as default in the DB
DEFAULT_ISSUE_TYPE = :issue
belongs_to :project
belongs_to :namespace, inverse_of: :issues
@ -713,6 +716,12 @@ class Issue < ApplicationRecord
project || namespace
end
# Persisted records will always have a work_item_type. This method is useful
# in places where we use a non persisted issue to perform feature checks
def work_item_type_with_default
work_item_type || WorkItems::Type.default_by_type(DEFAULT_ISSUE_TYPE)
end
private
def due_date_after_start_date
@ -783,6 +792,8 @@ class Issue < ApplicationRecord
def ensure_work_item_type
return if work_item_type_id.present? || work_item_type_id_change&.last.present?
# TODO: We should switch to DEFAULT_ISSUE_TYPE here when the issue_type column is dropped
# https://gitlab.com/gitlab-org/gitlab/-/work_items/402700?iid_path=true
self.work_item_type = WorkItems::Type.default_by_type(issue_type)
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module Packages
module Npm
class MetadataCache < ApplicationRecord
belongs_to :project, inverse_of: :npm_metadata_caches
validates :file, :package_name, :project, :size, presence: true
validates :package_name, uniqueness: { scope: :project_id }
validates :package_name, format: { with: Gitlab::Regex.package_name_regex }
validates :package_name, format: { with: Gitlab::Regex.npm_package_name_regex }
end
end
end

View File

@ -261,6 +261,8 @@ class Project < ApplicationRecord
has_many :debian_distributions,
class_name: 'Packages::Debian::ProjectDistribution',
dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :npm_metadata_caches,
class_name: 'Packages::Npm::MetadataCache'
has_one :packages_cleanup_policy,
class_name: 'Packages::Cleanup::Policy',
inverse_of: :project

View File

@ -36,7 +36,8 @@ class ProjectFeature < ApplicationRecord
merge_requests: Gitlab::Access::REPORTER,
metrics_dashboard: Gitlab::Access::REPORTER,
container_registry: Gitlab::Access::REPORTER,
package_registry: Gitlab::Access::REPORTER
package_registry: Gitlab::Access::REPORTER,
environments: Gitlab::Access::REPORTER
}.freeze
PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT = { repository: Gitlab::Access::REPORTER }.freeze

View File

@ -421,7 +421,6 @@ class ProjectPolicy < BasePolicy
end
rule { can?(:metrics_dashboard) }.policy do
enable :read_prometheus
enable :read_deployment
end

View File

@ -54,7 +54,15 @@ module MergeRequests
end
def validate_service
errors << 'User is required' if current_user.nil?
if current_user.nil?
errors << 'User is required'
return
end
unless current_user&.can?(:read_code, target_project)
errors << 'User access was denied'
return
end
unless target_project.merge_requests_enabled?
errors << "Merge requests are not enabled for project #{target_project.full_path}"

View File

@ -126,7 +126,7 @@
- if show_version_check?
.float-right
.js-gitlab-version-check-badge{ data: { "size": "lg", "actionable": "true", "version": gitlab_version_check.to_json } }
= link_to(sprite_icon('question'), "https://gitlab.com/gitlab-org/gitlab/-/blob/master/CHANGELOG.md", class: 'gl-ml-2', target: '_blank', rel: 'noopener noreferrer')
= link_to(sprite_icon('question-o'), "https://gitlab.com/gitlab-org/gitlab/-/blob/master/CHANGELOG.md", class: 'gl-ml-2', target: '_blank', rel: 'noopener noreferrer')
%p
= link_to _('GitLab'), general_admin_application_settings_path
%span.float-right

View File

@ -1,4 +1,3 @@
- @content_class = 'limit-container-width' unless fluid_layout
- add_to_breadcrumbs _('Kubernetes Clusters'), clusterable.index_path
- breadcrumb_title _('Connect a cluster')
- page_title _('Connect a Kubernetes Cluster')

View File

@ -1,4 +1,3 @@
- @content_class = 'limit-container-width' unless fluid_layout
- add_to_breadcrumbs _('Kubernetes Clusters'), clusterable.index_path
- breadcrumb_title _('Create a cluster')
- page_title _('Create a Kubernetes cluster')

View File

@ -1,4 +1,3 @@
- @content_class = "limit-container-width" unless fluid_layout
- add_to_breadcrumbs _('Kubernetes Clusters'), clusterable.index_path
- breadcrumb_title @cluster.name
- page_title _('Kubernetes Cluster')

View File

@ -8,5 +8,5 @@
%h5= _("Maximum page reached")
%p= _("Sorry, you have exceeded the maximum browsable page number. Please use the API to explore further.")
= render Pajamas::ButtonComponent.new(href: request.params.merge(page: @max_page_number)) do
= render Pajamas::ButtonComponent.new(href: safe_params.merge(page: @max_page_number)) do
= _("Back to page %{number}") % { number: @max_page_number }

View File

@ -2,7 +2,7 @@
.nav-block
.tree-ref-container
.tree-ref-holder
#js-tree-ref-switcher{ data: { project_id: @project.id, project_root_path: project_path(@project), ref: current_ref } }
#js-tree-ref-switcher{ data: { project_id: @project.id, project_root_path: project_path(@project), ref: current_ref, ref_type: @ref_type.to_s } }
%ul.breadcrumb.repo-breadcrumb
%li.breadcrumb-item

View File

@ -2,7 +2,7 @@
.tree-ref-container.gl-display-flex.gl-flex-wrap.gl-gap-2.mb-2.mb-md-0
.tree-ref-holder.gl-max-w-26
#js-tree-ref-switcher{ data: { project_id: @project.id, project_root_path: project_path(@project) } }
#js-tree-ref-switcher{ data: { project_id: @project.id, ref_type: @ref_type.to_s, project_root_path: project_path(@project) } }
#js-repo-breadcrumb{ data: breadcrumb_data_attributes }

View File

@ -0,0 +1,8 @@
---
name: db_load_balance_audit_event_streaming_worker
introduced_by_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1811
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/402493
milestone: '15.11'
type: development
group: group::scalability
default_enabled: false

View File

@ -0,0 +1,10 @@
---
table_name: packages_npm_metadata_caches
classes:
- Packages::Npm::MetadataCache
feature_categories:
- package_registry
description: Store the metadata of npm packages to use later as a cache
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114312
milestone: '15.11'
gitlab_schema: gitlab_main

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
class CreatePackagesNpmMetadataCaches < Gitlab::Database::Migration[2.1]
enable_lock_retries!
INDEX_NAME = 'index_npm_metadata_caches_on_package_name_project_id_unique'
def up
create_table :packages_npm_metadata_caches do |t|
t.timestamps_with_timezone
t.datetime_with_timezone :last_downloaded_at
t.bigint :project_id, index: true
t.integer :file_store, default: 1
t.integer :size, null: false
t.text :file, null: false, limit: 255
t.text :package_name, null: false # rubocop:disable Migration/AddLimitToTextColumns
t.index %i[package_name project_id], name: INDEX_NAME, unique: true, where: 'project_id IS NOT NULL'
end
end
def down
drop_table :packages_npm_metadata_caches
end
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddColumnToUsersStatistisc < Gitlab::Database::Migration[2.1]
def change
add_column :users_statistics, :with_highest_role_guest_with_custom_role, :integer, default: 0, null: false
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class AddProjectIdForeignKeyToPackagesNpmMetadataCaches < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
SOURCE_TABLE = :packages_npm_metadata_caches
TARGET_TABLE = :projects
COLUMN = :project_id
def up
add_concurrent_foreign_key SOURCE_TABLE, TARGET_TABLE, column: COLUMN, on_delete: :nullify
end
def down
with_lock_retries do
remove_foreign_key SOURCE_TABLE, column: COLUMN
end
end
end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
class NullifyLastErrorFromProjectMirrorData < Gitlab::Database::Migration[2.1]
MIGRATION = 'NullifyLastErrorFromProjectMirrorData'
INTERVAL = 2.minutes
BATCH_SIZE = 10_000
SUB_BATCH_SIZE = 1_000
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_main
def up
queue_batched_background_migration(
MIGRATION,
:project_mirror_data,
:id,
job_interval: INTERVAL,
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
end
def down
delete_batched_background_migration(MIGRATION, :project_mirror_data, :id, [])
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class RemoveTmpIndexVulnOccurrencesOnReportType < Gitlab::Database::Migration[2.1]
INDEX_NAME = 'tmp_idx_vulnerability_occurrences_on_id_where_report_type_7_99'
REPORT_TYPES = {
cluster_image_scanning: 7,
custom: 99
}
disable_ddl_transaction!
def up
remove_concurrent_index_by_name :vulnerability_occurrences, INDEX_NAME
end
def down
add_concurrent_index :vulnerability_occurrences, :id,
where: "report_type IN (#{REPORT_TYPES.values.join(', ')})",
name: INDEX_NAME
end
end

View File

@ -0,0 +1 @@
784f8f189eee7b5cf3136f0a859874a1d170d2b148f4c260f968b144816f1322

View File

@ -0,0 +1 @@
d8a040d40d19bb75c8e5fc8bb867ea6354ceda22c9dfe5724a4231a4b005e373

View File

@ -0,0 +1 @@
fc277fb3f02c01f57355cf6381a9883e6f67c339303242ea34c5a1b567b227d0

View File

@ -0,0 +1 @@
7ec944ccdd85380bba7d17fbd9dbf37bea918d0ac7fbe03142f4a4c6561a77a9

View File

@ -0,0 +1 @@
5bc014685295ca8af21450de34e39fb54e6cef2fc53943cce84ea24370a9955f

View File

@ -19561,6 +19561,28 @@ CREATE TABLE packages_npm_metadata (
CONSTRAINT chk_rails_e5cbc301ae CHECK ((char_length((package_json)::text) < 20000))
);
CREATE TABLE packages_npm_metadata_caches (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
last_downloaded_at timestamp with time zone,
project_id bigint,
file_store integer DEFAULT 1,
size integer NOT NULL,
file text NOT NULL,
package_name text NOT NULL,
CONSTRAINT check_57aa07a4b2 CHECK ((char_length(file) <= 255))
);
CREATE SEQUENCE packages_npm_metadata_caches_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE packages_npm_metadata_caches_id_seq OWNED BY packages_npm_metadata_caches.id;
CREATE TABLE packages_nuget_dependency_link_metadata (
dependency_link_id bigint NOT NULL,
target_framework text NOT NULL,
@ -23522,7 +23544,8 @@ CREATE TABLE users_statistics (
with_highest_role_owner integer DEFAULT 0 NOT NULL,
bots integer DEFAULT 0 NOT NULL,
blocked integer DEFAULT 0 NOT NULL,
with_highest_role_minimal_access integer DEFAULT 0 NOT NULL
with_highest_role_minimal_access integer DEFAULT 0 NOT NULL,
with_highest_role_guest_with_custom_role integer DEFAULT 0 NOT NULL
);
CREATE SEQUENCE users_statistics_id_seq
@ -25149,6 +25172,8 @@ ALTER TABLE ONLY packages_dependency_links ALTER COLUMN id SET DEFAULT nextval('
ALTER TABLE ONLY packages_maven_metadata ALTER COLUMN id SET DEFAULT nextval('packages_maven_metadata_id_seq'::regclass);
ALTER TABLE ONLY packages_npm_metadata_caches ALTER COLUMN id SET DEFAULT nextval('packages_npm_metadata_caches_id_seq'::regclass);
ALTER TABLE ONLY packages_package_file_build_infos ALTER COLUMN id SET DEFAULT nextval('packages_package_file_build_infos_id_seq'::regclass);
ALTER TABLE ONLY packages_package_files ALTER COLUMN id SET DEFAULT nextval('packages_package_files_id_seq'::regclass);
@ -27379,6 +27404,9 @@ ALTER TABLE ONLY packages_helm_file_metadata
ALTER TABLE ONLY packages_maven_metadata
ADD CONSTRAINT packages_maven_metadata_pkey PRIMARY KEY (id);
ALTER TABLE ONLY packages_npm_metadata_caches
ADD CONSTRAINT packages_npm_metadata_caches_pkey PRIMARY KEY (id);
ALTER TABLE ONLY packages_npm_metadata
ADD CONSTRAINT packages_npm_metadata_pkey PRIMARY KEY (package_id);
@ -31223,6 +31251,8 @@ CREATE INDEX index_notification_settings_on_source_and_level_and_user ON notific
CREATE UNIQUE INDEX index_notifications_on_user_id_and_source_id_and_source_type ON notification_settings USING btree (user_id, source_id, source_type);
CREATE UNIQUE INDEX index_npm_metadata_caches_on_package_name_project_id_unique ON packages_npm_metadata_caches USING btree (package_name, project_id) WHERE (project_id IS NOT NULL);
CREATE INDEX index_ns_root_stor_stats_on_registry_size_estimated ON namespace_root_storage_statistics USING btree (registry_size_estimated);
CREATE UNIQUE INDEX index_ns_user_callouts_feature ON user_namespace_callouts USING btree (user_id, feature_name, namespace_id);
@ -31373,6 +31403,8 @@ CREATE INDEX index_packages_maven_metadata_on_package_id_and_path ON packages_ma
CREATE INDEX index_packages_maven_metadata_on_path ON packages_maven_metadata USING btree (path);
CREATE INDEX index_packages_npm_metadata_caches_on_project_id ON packages_npm_metadata_caches USING btree (project_id);
CREATE INDEX index_packages_nuget_dl_metadata_on_dependency_link_id ON packages_nuget_dependency_link_metadata USING btree (dependency_link_id);
CREATE INDEX index_packages_on_available_pypi_packages ON packages_packages USING btree (project_id, id) WHERE ((status = ANY (ARRAY[0, 1])) AND (package_type = 5) AND (version IS NOT NULL));
@ -32743,8 +32775,6 @@ CREATE INDEX tmp_idx_for_vulnerability_feedback_migration ON vulnerability_feedb
CREATE INDEX tmp_idx_package_files_on_non_zero_size ON packages_package_files USING btree (package_id, size) WHERE (size IS NOT NULL);
CREATE INDEX tmp_idx_vulnerability_occurrences_on_id_where_report_type_7_99 ON vulnerability_occurrences USING btree (id) WHERE (report_type = ANY (ARRAY[7, 99]));
CREATE INDEX tmp_index_ci_job_artifacts_on_expire_at_where_locked_unknown ON ci_job_artifacts USING btree (expire_at, job_id) WHERE ((locked = 2) AND (expire_at IS NOT NULL));
CREATE INDEX tmp_index_ci_job_artifacts_on_id_expire_at_file_type_trace ON ci_job_artifacts USING btree (id) WHERE (((date_part('day'::text, timezone('UTC'::text, expire_at)) = ANY (ARRAY[(21)::double precision, (22)::double precision, (23)::double precision])) AND (date_part('minute'::text, timezone('UTC'::text, expire_at)) = ANY (ARRAY[(0)::double precision, (30)::double precision, (45)::double precision])) AND (date_part('second'::text, timezone('UTC'::text, expire_at)) = (0)::double precision)) OR (file_type = 3));
@ -34828,6 +34858,9 @@ ALTER TABLE ONLY merge_requests
ALTER TABLE ONLY ml_experiments
ADD CONSTRAINT fk_ad89c59858 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY packages_npm_metadata_caches
ADD CONSTRAINT fk_ada23b1d30 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE SET NULL;
ALTER TABLE ONLY merge_request_metrics
ADD CONSTRAINT fk_ae440388cc FOREIGN KEY (latest_closed_by_id) REFERENCES users(id) ON DELETE SET NULL;

View File

@ -163,7 +163,8 @@ The following API resources are available outside of project and group contexts
| [Geo Nodes](geo_nodes.md) **(PREMIUM SELF)** | `/geo_nodes` |
| [Group Activity Analytics](group_activity_analytics.md) | `/analytics/group_activity/{issues_count}` |
| [Group repository storage moves](group_repository_storage_moves.md) **(PREMIUM SELF)** | `/group_repository_storage_moves` |
| [Import repository from GitHub](import.md) | `/import/github` |
| [Import repository from GitHub](import.md#import-repository-from-github) | `/import/github` |
| [Import repository from Bitbucket Server](import.md#import-repository-from-bitbucket-server) | `/import/bitbucket_server` |
| [Instance clusters](instance_clusters.md) **(FREE SELF)** | `/admin/clusters` |
| [Instance-level CI/CD variables](instance_level_ci_variables.md) **(FREE SELF)** | `/admin/ci/variables` |
| [Issues Statistics](issues_statistics.md) | `/issues_statistics` (also available for groups and projects) |

View File

@ -7028,7 +7028,7 @@ references and their corresponding code points.</p>
<copy-code></copy-code>
</div>
<div class="gl-relative markdown-code-block js-markdown-code">
<pre data-sourcepos="7591:1-7595:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">&lt;p&gt;  &amp;amp; © Æ Ď</span>
<pre data-sourcepos="7591:1-7595:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">&lt;p&gt; &amp;amp; © Æ Ď</span>
<span id="LC2" class="line" lang="plaintext">¾ </span>
<span id="LC3" class="line" lang="plaintext">∲ ≧̸&lt;/p&gt;</span></code></pre>
<copy-code></copy-code>
@ -7344,11 +7344,11 @@ stripped in this way:</p>
<div>
<div><a href="#example-343">Example 343</a></div>
<div class="gl-relative markdown-code-block js-markdown-code">
<pre data-sourcepos="7960:1-7962:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">` b `</span></code></pre>
<pre data-sourcepos="7960:1-7962:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">` b `</span></code></pre>
<copy-code></copy-code>
</div>
<div class="gl-relative markdown-code-block js-markdown-code">
<pre data-sourcepos="7964:1-7966:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">&lt;p&gt;&lt;code&gt; b &lt;/code&gt;&lt;/p&gt;</span></code></pre>
<pre data-sourcepos="7964:1-7966:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">&lt;p&gt;&lt;code&gt; b &lt;/code&gt;&lt;/p&gt;</span></code></pre>
<copy-code></copy-code>
</div>
</div>
@ -7356,12 +7356,12 @@ stripped in this way:</p>
<div>
<div><a href="#example-344">Example 344</a></div>
<div class="gl-relative markdown-code-block js-markdown-code">
<pre data-sourcepos="7974:1-7977:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">` `</span>
<pre data-sourcepos="7974:1-7977:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">` `</span>
<span id="LC2" class="line" lang="plaintext">` `</span></code></pre>
<copy-code></copy-code>
</div>
<div class="gl-relative markdown-code-block js-markdown-code">
<pre data-sourcepos="7979:1-7982:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">&lt;p&gt;&lt;code&gt; &lt;/code&gt;</span>
<pre data-sourcepos="7979:1-7982:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">&lt;p&gt;&lt;code&gt; &lt;/code&gt;</span>
<span id="LC2" class="line" lang="plaintext">&lt;code&gt; &lt;/code&gt;&lt;/p&gt;</span></code></pre>
<copy-code></copy-code>
</div>
@ -7832,11 +7832,11 @@ not part of a [left-flanking delimiter run]:</p>
<div>
<div><a href="#example-363">Example 363</a></div>
<div class="gl-relative markdown-code-block js-markdown-code">
<pre data-sourcepos="8485:1-8487:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">* a *</span></code></pre>
<pre data-sourcepos="8485:1-8487:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">* a *</span></code></pre>
<copy-code></copy-code>
</div>
<div class="gl-relative markdown-code-block js-markdown-code">
<pre data-sourcepos="8489:1-8491:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">&lt;p&gt;* a *&lt;/p&gt;</span></code></pre>
<pre data-sourcepos="8489:1-8491:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">&lt;p&gt;* a *&lt;/p&gt;</span></code></pre>
<copy-code></copy-code>
</div>
</div>
@ -9790,7 +9790,7 @@ Other [Unicode whitespace] like non-breaking space doesn't work.</p>
<div>
<div><a href="#example-515">Example 515</a></div>
<div class="gl-relative markdown-code-block js-markdown-code">
<pre data-sourcepos="10823:1-10825:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">[link](/url "title")</span></code></pre>
<pre data-sourcepos="10823:1-10825:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">[link](/url "title")</span></code></pre>
<copy-code></copy-code>
</div>
<div class="gl-relative markdown-code-block js-markdown-code">

View File

@ -203,6 +203,10 @@ module API
render_api_error!("Target project id:#{params[:from_project_id]} is not a fork of project id:#{params[:id]}", 400)
end
unless can?(current_user, :read_code, target_project)
forbidden!("You don't have access to this fork's parent project")
end
cache_key = compare_cache_key(current_user, user_project, target_project, declared_params)
cache_action(cache_key, expires_in: 1.minute) do

View File

@ -58,6 +58,7 @@ module Banzai
def records_for_nodes(nodes)
node_includes = [
:work_item_type,
:namespace,
:author,
:assignees,

View File

@ -5,7 +5,8 @@
# Can be extended for different types of repository object, e.g. Project or Snippet
module ExtractsRef
InvalidPathError = Class.new(StandardError)
BRANCH_REF_TYPE = 'heads'
TAG_REF_TYPE = 'tags'
# Given a string containing both a Git tree-ish, such as a branch or tag, and
# a filesystem path joined by forward slashes, attempts to separate the two.
#
@ -91,7 +92,7 @@ module ExtractsRef
def ref_type
return unless params[:ref_type].present?
params[:ref_type] == 'tags' ? 'tags' : 'heads'
params[:ref_type] == TAG_REF_TYPE ? TAG_REF_TYPE : BRANCH_REF_TYPE
end
private
@ -154,4 +155,13 @@ module ExtractsRef
def repository_container
raise NotImplementedError
end
def ambiguous_ref?(project, ref)
return true if project.repository.ambiguous_ref?(ref)
return false unless ref&.starts_with?('refs/')
unprefixed_ref = ref.sub(%r{^refs/(heads|tags)/}, '')
project.repository.commit(unprefixed_ref).present?
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# Nullifies last_error value from project_mirror_data table as they
# potentially included sensitive data.
# https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/3041
class NullifyLastErrorFromProjectMirrorData < BatchedMigrationJob
feature_category :source_code_management
operation_name :update_all
def perform
each_sub_batch { |rel| rel.update_all(last_error: nil) }
end
end
end
end

View File

@ -453,6 +453,17 @@ module Gitlab
)
}mx.freeze
# Code blocks:
# ```
# Anything, including `>>>` blocks which are ignored by this filter
# ```
MARKDOWN_CODE_BLOCK_REGEX_UNTRUSTED =
'(?P<code>' \
'^```\n' \
'(?:\n|.)*?' \
'\n```\ *$' \
')'.freeze
MARKDOWN_HTML_BLOCK_REGEX = %r{
(?<html>
# HTML block:
@ -466,27 +477,19 @@ module Gitlab
)
}mx.freeze
MARKDOWN_HTML_COMMENT_LINE_REGEX = %r{
(?<html_comment_line>
# HTML comment line:
# <!-- some commented text -->
# HTML comment line:
# <!-- some commented text -->
MARKDOWN_HTML_COMMENT_LINE_REGEX_UNTRUSTED =
'(?P<html_comment_line>' \
'^<!--\ .*?\ -->\ *$' \
')'.freeze
^<!--\ .*?\ -->\ *$
)
}mx.freeze
MARKDOWN_HTML_COMMENT_BLOCK_REGEX = %r{
(?<html_comment_block>
# HTML comment block:
# <!-- some commented text
# additional text
# -->
^<!--.*\n
.+?
\n-->\ *$
)
}mx.freeze
MARKDOWN_HTML_COMMENT_BLOCK_REGEX_UNTRUSTED =
'(?P<html_comment_block>' \
'^<!--.*?\n' \
'(?:\n|.)*?' \
'\n.*?-->\ *$' \
')'.freeze
def markdown_code_or_html_blocks
@markdown_code_or_html_blocks ||= %r{
@ -496,14 +499,13 @@ module Gitlab
}mx.freeze
end
def markdown_code_or_html_comments
@markdown_code_or_html_comments ||= %r{
#{MARKDOWN_CODE_BLOCK_REGEX}
|
#{MARKDOWN_HTML_COMMENT_LINE_REGEX}
|
#{MARKDOWN_HTML_COMMENT_BLOCK_REGEX}
}mx.freeze
def markdown_code_or_html_comments_untrusted
@markdown_code_or_html_comments_untrusted ||=
"#{MARKDOWN_CODE_BLOCK_REGEX_UNTRUSTED}" \
"|" \
"#{MARKDOWN_HTML_COMMENT_LINE_REGEX_UNTRUSTED}" \
"|" \
"#{MARKDOWN_HTML_COMMENT_BLOCK_REGEX_UNTRUSTED}"
end
# Based on Jira's project key format

View File

@ -9,6 +9,12 @@ module Gitlab
# https://idiosyncratic-ruby.com/41-proper-unicoding.html
BIDI_REGEXP = /\p{Bidi Control}/.freeze
# Regular expression for identifying space characters
#
# In web browsers space characters can be confused with simple
# spaces which may be misleading
SPACE_REGEXP = /\p{Space_Separator}/.freeze
class << self
# Warning message used to highlight bidi characters in the GUI
def bidi_warning

View File

@ -47,6 +47,17 @@ module Gitlab
RE2.Replace(text, regexp, rewrite)
end
# #scan returns an array of the groups captured, rather than MatchData.
# Use this to give the capture group name and grab the proper value
def extract_named_group(name, match)
return unless match
match_position = regexp.named_capturing_groups[name.to_s]
raise RegexpError, "Invalid named capture group: #{name}" unless match_position
match[match_position - 1]
end
def ==(other)
self.source == other.source
end

View File

@ -2,15 +2,37 @@
module Gitlab
class UrlSanitizer
include Gitlab::Utils::StrongMemoize
ALLOWED_SCHEMES = %w[http https ssh git].freeze
ALLOWED_WEB_SCHEMES = %w[http https].freeze
SCHEMIFIED_SCHEME = 'glschemelessuri'
SCHEMIFY_PLACEHOLDER = "#{SCHEMIFIED_SCHEME}://".freeze
# URI::DEFAULT_PARSER.make_regexp will only match URLs with schemes or
# relative URLs. This section will match schemeless URIs with userinfo
# e.g. user:pass@gitlab.com but will not match scp-style URIs e.g.
# user@server:path/to/file)
#
# The userinfo part is very loose compared to URI's implementation so we
# also match non-escaped userinfo e.g foo:b?r@gitlab.com which should be
# encoded as foo:b%3Fr@gitlab.com
URI_REGEXP = %r{
(?:
#{URI::DEFAULT_PARSER.make_regexp(ALLOWED_SCHEMES)}
|
(?:(?:(?!@)[%#{URI::REGEXP::PATTERN::UNRESERVED}#{URI::REGEXP::PATTERN::RESERVED}])+(?:@))
(?# negative lookahead ensures this isn't an SCP-style URL: [host]:[rel_path|abs_path] server:path/to/file)
(?!#{URI::REGEXP::PATTERN::HOST}:(?:#{URI::REGEXP::PATTERN::REL_PATH}|#{URI::REGEXP::PATTERN::ABS_PATH}))
#{URI::REGEXP::PATTERN::HOSTPORT}
)
}x
def self.sanitize(content)
regexp = URI::DEFAULT_PARSER.make_regexp(ALLOWED_SCHEMES)
content.gsub(regexp) { |url| new(url).masked_url }
rescue Addressable::URI::InvalidURIError
content.gsub(regexp, '')
content.gsub(URI_REGEXP) do |url|
new(url).masked_url
rescue Addressable::URI::InvalidURIError
''
end
end
def self.valid?(url, allowed_schemes: ALLOWED_SCHEMES)
@ -37,17 +59,6 @@ module Gitlab
@url = parse_url(url)
end
def sanitized_url
@sanitized_url ||= safe_url.to_s
end
def masked_url
url = @url.dup
url.password = "*****" if url.password.present?
url.user = "*****" if url.user.present?
url.to_s
end
def credentials
@credentials ||= { user: @url.user.presence, password: @url.password.presence }
end
@ -56,15 +67,37 @@ module Gitlab
credentials[:user]
end
def full_url
@full_url ||= generate_full_url.to_s
def sanitized_url
safe_url = @url.dup
safe_url.password = nil
safe_url.user = nil
reverse_schemify(safe_url.to_s)
end
strong_memoize_attr :sanitized_url
def masked_url
url = @url.dup
url.password = "*****" if url.password.present?
url.user = "*****" if url.user.present?
reverse_schemify(url.to_s)
end
strong_memoize_attr :masked_url
def full_url
return reverse_schemify(@url.to_s) unless valid_credentials?
url = @url.dup
url.password = encode_percent(credentials[:password]) if credentials[:password].present?
url.user = encode_percent(credentials[:user]) if credentials[:user].present?
reverse_schemify(url.to_s)
end
strong_memoize_attr :full_url
private
def parse_url(url)
url = url.to_s.strip
match = url.match(%r{\A(?:git|ssh|http(?:s?))\://(?:(.+)(?:@))?(.+)})
url = schemify(url.to_s.strip)
match = url.match(%r{\A(?:(?:#{SCHEMIFIED_SCHEME}|git|ssh|http(?:s?)):)?//(?:(.+)(?:@))?(.+)}o)
raw_credentials = match[1] if match
if raw_credentials.present?
@ -83,24 +116,19 @@ module Gitlab
url
end
def generate_full_url
return @url unless valid_credentials?
@url.dup.tap do |generated|
generated.password = encode_percent(credentials[:password]) if credentials[:password].present?
generated.user = encode_percent(credentials[:user]) if credentials[:user].present?
end
def schemify(url)
# Prepend the placeholder scheme unless the URL has a scheme or is relative
url.prepend(SCHEMIFY_PLACEHOLDER) unless url.starts_with?(%r{(?:#{URI::REGEXP::PATTERN::SCHEME}:)?//}o)
url
end
def safe_url
safe_url = @url.dup
safe_url.password = nil
safe_url.user = nil
safe_url
def reverse_schemify(url)
url.slice!(SCHEMIFY_PLACEHOLDER) if url.starts_with?(SCHEMIFY_PLACEHOLDER)
url
end
def valid_credentials?
credentials && credentials.is_a?(Hash) && credentials.any?
credentials.is_a?(Hash) && credentials.values.any?
end
def encode_percent(string)

View File

@ -25,7 +25,10 @@ module Rouge
yield %(<span id="LC#{@line_number}" class="line" lang="#{@tag}">)
line.each do |token, value|
yield highlight_unicode_control_characters(span(token, value.chomp! || value))
value = value.chomp! || value
value = replace_space_characters(value)
yield highlight_unicode_control_characters(span(token, value))
end
yield ellipsis if @ellipsis_indexes.include?(@line_number - 1) && @ellipsis_svg.present?
@ -42,6 +45,10 @@ module Rouge
%(<span class="gl-px-2 gl-rounded-base gl-mx-2 gl-bg-gray-100 gl-cursor-help has-tooltip" title="Content has been trimmed">#{@ellipsis_svg}</span>)
end
def replace_space_characters(text)
text.gsub(Gitlab::Unicode::SPACE_REGEXP, ' ')
end
def highlight_unicode_control_characters(text)
text.gsub(Gitlab::Unicode::BIDI_REGEXP) do |char|
%(<span class="unicode-bidi has-tooltip" data-toggle="tooltip" title="#{Gitlab::Unicode.bidi_warning}">#{char}</span>)

View File

@ -2775,6 +2775,9 @@ msgstr ""
msgid "AdminArea|Users with highest role"
msgstr ""
msgid "AdminArea|Users with highest role guest and with a %{strongOpen}Custom Role%{strongClose}."
msgstr ""
msgid "AdminArea|Users without a Group and Project"
msgstr ""
@ -27322,6 +27325,9 @@ msgstr ""
msgid "MergeRequest|Approved by @%{username}"
msgstr ""
msgid "MergeRequest|Can't fetch the single file diff for the discussion. Please reload this page."
msgstr ""
msgid "MergeRequest|Can't show this merge request because of an internal error. Contact your administrator."
msgstr ""
@ -43764,6 +43770,9 @@ msgstr ""
msgid "The default branch for this project has been changed. Please update your bookmarks."
msgstr ""
msgid "The default branch of this project clashes with another ref"
msgstr ""
msgid "The dependency list details information about the components used within your project."
msgstr ""

View File

@ -11,7 +11,6 @@ module QA
element :ci_variable_key_field
element :ci_variable_value_field
element :ci_variable_save_button
element :ci_variable_delete_button
end
def fill_variable(key, value, masked = false)
@ -37,14 +36,6 @@ module QA
def click_ci_variable_save_button
click_element :ci_variable_save_button
end
def click_reveal_ci_variable_value_button
click_element :reveal_ci_variable_value_button
end
def click_ci_variable_delete_button
click_element :ci_variable_delete_button
end
end
end
end

View File

@ -18,19 +18,6 @@ module QA
@variable_type = 'env_var'
end
def fabricate!
project.visit!
Page::Project::Menu.perform(&:go_to_ci_cd_settings)
Page::Project::Settings::CiCd.perform do |setting|
setting.expand_ci_variables do |page|
page.click_add_variable
page.fill_variable(key, value, masked)
end
end
end
def fabricate_via_api!
resource_web_url(api_get)
rescue ResourceNotFoundError

View File

@ -1,51 +0,0 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Verify' do
describe 'Add or Remove CI variable via UI', :smoke, product_group: :pipeline_security do
let(:project) do
Resource::Project.fabricate_via_api_unless_fips! do |project|
project.name = 'project-with-ci-variables'
project.description = 'project with CI variables'
end
end
before do
Flow::Login.sign_in
project.visit!
add_ci_variable
end
it 'user adds a CI variable', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348027' do
Page::Project::Settings::CiVariables.perform do |ci_variable|
expect(ci_variable).to have_text('VARIABLE_KEY')
expect(ci_variable).not_to have_text('some_CI_variable')
ci_variable.click_reveal_ci_variable_value_button
expect(ci_variable).to have_text('some_CI_variable')
end
end
it 'user removes a CI variable', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348026' do
Page::Project::Settings::CiVariables.perform do |ci_variable|
ci_variable.click_edit_ci_variable
ci_variable.click_ci_variable_delete_button
expect(ci_variable).to have_text('There are no variables yet', wait: 60)
end
end
private
def add_ci_variable
Resource::CiVariable.fabricate_via_browser_ui! do |ci_variable|
ci_variable.project = project
ci_variable.key = 'VARIABLE_KEY'
ci_variable.value = 'some_CI_variable'
ci_variable.masked = false
end
end
end
end
end

View File

@ -59,6 +59,7 @@ RSpec.describe Admin::HooksController do
enable_ssl_verification: false,
url_variables: [
{ key: 'token', value: 'some secret value' },
{ key: 'baz', value: 'qux' },
{ key: 'foo', value: nil }
]
}
@ -71,7 +72,7 @@ RSpec.describe Admin::HooksController do
expect(flash[:notice]).to include('was updated')
expect(hook).to have_attributes(hook_params.except(:url_variables))
expect(hook).to have_attributes(
url_variables: { 'token' => 'some secret value', 'baz' => 'woo' }
url_variables: { 'token' => 'some secret value', 'baz' => 'qux' }
)
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe ConfirmEmailWarning do
RSpec.describe ConfirmEmailWarning, feature_category: :system_access do
before do
stub_application_setting_enum('email_confirmation_setting', 'soft')
end
@ -82,6 +82,38 @@ RSpec.describe ConfirmEmailWarning do
it { is_expected.to set_confirm_warning_for(user.email) }
end
end
context 'when user is being impersonated' do
let(:impersonator) { create(:admin) }
before do
allow(controller).to receive(:session).and_return({ impersonator_id: impersonator.id })
get :index
end
it { is_expected.to set_confirm_warning_for(user.email) }
context 'when impersonated user email has html in their email' do
let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: "malicious@test.com<form><input/title='<script>alert(document.domain)</script>'>") }
it { is_expected.to set_confirm_warning_for("malicious@test.com&lt;form&gt;&lt;input/title=&#39;&lt;script&gt;alert(document.domain)&lt;/script&gt;&#39;&gt;") }
end
end
context 'when user is not being impersonated' do
before do
get :index
end
it { is_expected.to set_confirm_warning_for(user.email) }
context 'when user email has html in their email' do
let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: "malicious@test.com<form><input/title='<script>alert(document.domain)</script>'>") }
it { is_expected.to set_confirm_warning_for("malicious@test.com&lt;form&gt;&lt;input/title=&#39;&lt;script&gt;alert(document.domain)&lt;/script&gt;&#39;&gt;") }
end
end
end
end
end

View File

@ -2,15 +2,16 @@
require 'spec_helper'
RSpec.describe Projects::BlobController do
RSpec.describe Projects::BlobController, feature_category: :source_code_management do
include ProjectForksHelper
let(:project) { create(:project, :public, :repository, previous_default_branch: previous_default_branch) }
let(:previous_default_branch) { nil }
describe "GET show" do
def request
get(:show, params: { namespace_id: project.namespace, project_id: project, id: id })
let(:params) { { namespace_id: project.namespace, project_id: project, id: id } }
let(:request) do
get(:show, params: params)
end
render_views
@ -18,10 +19,34 @@ RSpec.describe Projects::BlobController do
context 'with file path' do
before do
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
project.repository.add_tag(project.creator, 'ambiguous_ref', RepoHelpers.sample_commit.id)
project.repository.add_branch(project.creator, 'ambiguous_ref', RepoHelpers.another_sample_commit.id)
request
end
context 'when the ref is ambiguous' do
let(:ref) { 'ambiguous_ref' }
let(:path) { 'README.md' }
let(:id) { "#{ref}/#{path}" }
let(:params) { { namespace_id: project.namespace, project_id: project, id: id, ref_type: ref_type } }
context 'and explicitly requesting a branch' do
let(:ref_type) { 'heads' }
it 'redirects to blob#show with sha for the branch' do
expect(response).to redirect_to(project_blob_path(project, "#{RepoHelpers.another_sample_commit.id}/#{path}"))
end
end
context 'and explicitly requesting a tag' do
let(:ref_type) { 'tags' }
it 'responds with success' do
expect(response).to be_ok
end
end
end
context "valid branch, valid file" do
let(:id) { 'master/README.md' }

View File

@ -7,7 +7,7 @@ RSpec.describe Projects::ClustersController, feature_category: :kubernetes_manag
include GoogleApi::CloudPlatformHelpers
include KubernetesHelpers
let_it_be(:project) { create(:project) }
let_it_be_with_reload(:project) { create(:project) }
let(:user) { create(:user) }
@ -140,6 +140,27 @@ RSpec.describe Projects::ClustersController, feature_category: :kubernetes_manag
expect(response).to redirect_to(new_user_session_path)
end
end
context 'with a public project' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
project.project_feature.update!(metrics_dashboard_access_level: ProjectFeature::ENABLED)
end
context 'with guest user' do
let(:prometheus_body) { nil }
before do
project.add_guest(user)
end
it 'returns 404' do
get :prometheus_proxy, params: prometheus_proxy_params
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
end

View File

@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Projects::Environments::PrometheusApiController do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be_with_reload(:project) { create(:project) }
let_it_be(:proxyable) { create(:environment, project: project) }
before do
@ -70,6 +70,27 @@ RSpec.describe Projects::Environments::PrometheusApiController do
expect(response).to redirect_to(new_user_session_path)
end
end
context 'with a public project' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
project.project_feature.update!(metrics_dashboard_access_level: ProjectFeature::ENABLED)
end
context 'with guest user' do
let(:prometheus_body) { nil }
before do
project.add_guest(user)
end
it 'returns 404' do
get :prometheus_proxy, params: prometheus_proxy_params
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Projects::TreeController do
RSpec.describe Projects::TreeController, feature_category: :source_code_management do
let(:project) { create(:project, :repository, previous_default_branch: previous_default_branch) }
let(:previous_default_branch) { nil }
let(:user) { create(:user) }
@ -15,15 +15,41 @@ RSpec.describe Projects::TreeController do
end
describe "GET show" do
let(:params) do
{
namespace_id: project.namespace.to_param, project_id: project, id: id
}
end
# Make sure any errors accessing the tree in our views bubble up to this spec
render_views
before do
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
project.repository.add_tag(project.creator, 'ambiguous_ref', RepoHelpers.sample_commit.id)
project.repository.add_branch(project.creator, 'ambiguous_ref', RepoHelpers.another_sample_commit.id)
get :show, params: params
end
get :show, params: {
namespace_id: project.namespace.to_param, project_id: project, id: id
}
context 'when the ref is ambiguous' do
let(:id) { 'ambiguous_ref' }
let(:params) { { namespace_id: project.namespace, project_id: project, id: id, ref_type: ref_type } }
context 'and explicitly requesting a branch' do
let(:ref_type) { 'heads' }
it 'redirects to blob#show with sha for the branch' do
expect(response).to redirect_to(project_tree_path(project, RepoHelpers.another_sample_commit.id))
end
end
context 'and explicitly requesting a tag' do
let(:ref_type) { 'tags' }
it 'responds with success' do
expect(response).to be_ok
end
end
end
context "valid branch, no path" do

View File

@ -163,6 +163,69 @@ RSpec.describe ProjectsController, feature_category: :projects do
expect(assigns(:notification_setting).level).to eq("watch")
end
end
context 'when there is a tag with the same name as the default branch' do
let_it_be(:tagged_project) { create(:project, :public, :custom_repo, files: ['somefile']) }
let(:tree_with_default_branch) do
branch = tagged_project.repository.find_branch(tagged_project.default_branch)
project_tree_path(tagged_project, branch.target)
end
before do
tagged_project.repository.create_file(
tagged_project.creator,
'file_for_tag',
'content for file',
message: "Automatically created file",
branch_name: 'branch-to-tag'
)
tagged_project.repository.add_tag(
tagged_project.creator,
tagged_project.default_branch, # tag name
'branch-to-tag' # target
)
end
it 'redirects to tree view for the default branch' do
get :show, params: { namespace_id: tagged_project.namespace, id: tagged_project }
expect(response).to redirect_to(tree_with_default_branch)
end
end
context 'when the default branch name can resolve to another ref' do
let!(:project_with_default_branch) do
create(:project, :public, :custom_repo, files: ['somefile']).tap do |p|
p.repository.create_branch("refs/heads/refs/heads/#{other_ref}", 'master')
p.change_head("refs/heads/#{other_ref}")
end.reload
end
let(:other_ref) { 'branch-name' }
context 'but there is no other ref' do
it 'responds with ok' do
get :show, params: { namespace_id: project_with_default_branch.namespace, id: project_with_default_branch }
expect(response).to be_ok
end
end
context 'and that other ref exists' do
let(:tree_with_default_branch) do
branch = project_with_default_branch.repository.find_branch(project_with_default_branch.default_branch)
project_tree_path(project_with_default_branch, branch.target)
end
before do
project_with_default_branch.repository.create_branch(other_ref, 'master')
end
it 'redirects to tree view for the default branch' do
get :show, params: { namespace_id: project_with_default_branch.namespace, id: project_with_default_branch }
expect(response).to redirect_to(tree_with_default_branch)
end
end
end
end
describe "when project repository is disabled" do

View File

@ -66,6 +66,11 @@ FactoryBot.define do
end
end
trait :requirement do
issue_type { :requirement }
association :work_item_type, :default, :requirement
end
trait :task do
issue_type { :task }
association :work_item_type, :default, :task

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
FactoryBot.define do
factory :npm_metadata_cache, class: 'Packages::Npm::MetadataCache' do
project
sequence(:package_name) { |n| "@#{project.root_namespace.path}/package-#{n}" }
file { 'unnamed' }
size { 100.kilobytes }
end
end

View File

@ -7,7 +7,7 @@ FactoryBot.define do
project
trait :url_variables do
url_variables { { 'abc' => 'supers3cret' } }
url_variables { { 'abc' => 'supers3cret', 'def' => 'foobar' } }
end
trait :token do

View File

@ -271,6 +271,36 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do
icon = first('[data-testid="incognito-icon"]')
expect(icon).not_to be nil
end
context 'when viewing the confirm email warning', :js do
let_it_be(:another_user) { create(:user, :unconfirmed) }
let(:warning_alert) { page.find(:css, '[data-testid="alert-warning"]') }
let(:expected_styling) { { 'pointer-events' => 'none', 'cursor' => 'default' } }
context 'with an email that does not contain HTML' do
before do
subject
end
it 'displays the warning alert including the email' do
expect(warning_alert.text).to include("Please check your email (#{another_user.email}) to verify")
end
end
context 'with an email that contains HTML' do
let(:malicious_email) { "malicious@test.com<form><input/title='<script>alert(document.domain)</script>'>" }
let(:another_user) { create(:user, confirmed_at: nil, unconfirmed_email: malicious_email) }
before do
subject
end
it 'displays the impersonation alert, excludes email, and disables links' do
expect(warning_alert.text).to include("check your email (#{another_user.unconfirmed_email}) to verify")
end
end
end
end
context 'ending impersonation' do

View File

@ -208,17 +208,14 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
describe 'with two-factor authentication', :js do
def enter_code(code)
if page.has_content?("Sign in via 2FA code")
click_on("Sign in via 2FA code")
enter_code(code)
else
fill_in 'user_otp_attempt', with: code
click_button 'Verify code'
end
def enter_code(code, only_two_factor_webauthn_enabled: false)
click_on("Sign in via 2FA code") if only_two_factor_webauthn_enabled
fill_in 'user_otp_attempt', with: code
click_button 'Verify code'
end
shared_examples_for 'can login with recovery codes' do
shared_examples_for 'can login with recovery codes' do |only_two_factor_webauthn_enabled: false|
context 'using backup code' do
let(:codes) { user.generate_otp_backup_codes! }
@ -235,7 +232,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
.to increment(:user_authenticated_counter)
.and increment(:user_two_factor_authenticated_counter)
enter_code(codes.sample)
enter_code(codes.sample, only_two_factor_webauthn_enabled: only_two_factor_webauthn_enabled)
expect(page).to have_current_path root_path, ignore_query: true
end
@ -245,7 +242,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
.to increment(:user_authenticated_counter)
.and increment(:user_two_factor_authenticated_counter)
expect { enter_code(codes.sample) }
expect { enter_code(codes.sample, only_two_factor_webauthn_enabled: only_two_factor_webauthn_enabled) }
.to change { user.reload.otp_backup_codes.size }.by(-1)
end
@ -256,13 +253,13 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
.and increment(:user_session_destroyed_counter)
random_code = codes.delete(codes.sample)
expect { enter_code(random_code) }
expect { enter_code(random_code, only_two_factor_webauthn_enabled: only_two_factor_webauthn_enabled) }
.to change { user.reload.otp_backup_codes.size }.by(-1)
gitlab_sign_out
gitlab_sign_in(user)
expect { enter_code(codes.sample) }
expect { enter_code(codes.sample, only_two_factor_webauthn_enabled: only_two_factor_webauthn_enabled) }
.to change { user.reload.otp_backup_codes.size }.by(-1)
end
@ -272,7 +269,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
.and increment(:user_two_factor_authenticated_counter)
expect(ActiveSession).to receive(:cleanup).with(user).once.and_call_original
enter_code(codes.sample)
enter_code(codes.sample, only_two_factor_webauthn_enabled: only_two_factor_webauthn_enabled)
end
end
@ -287,7 +284,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
user.save!(touch: false)
expect(user.reload.otp_backup_codes.size).to eq 9
enter_code(code)
enter_code(code, only_two_factor_webauthn_enabled: only_two_factor_webauthn_enabled)
expect(page).to have_content('Invalid two-factor code.')
end
end
@ -382,7 +379,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
context 'when user with only Webauthn enabled' do
let(:user) { create(:user, :two_factor_via_webauthn, registrations_count: 1) }
include_examples 'can login with recovery codes'
include_examples 'can login with recovery codes', only_two_factor_webauthn_enabled: true
end
end

View File

@ -6,6 +6,7 @@ RSpec.describe Environments::EnvironmentNamesFinder do
describe '#execute' do
let!(:group) { create(:group) }
let!(:public_project) { create(:project, :public, namespace: group) }
let_it_be_with_reload(:public_project_with_private_environments) { create(:project, :public) }
let!(:private_project) { create(:project, :private, namespace: group) }
let!(:user) { create(:user) }
@ -14,6 +15,11 @@ RSpec.describe Environments::EnvironmentNamesFinder do
create(:environment, name: 'gprd', project: public_project)
create(:environment, name: 'gprd', project: private_project)
create(:environment, name: 'gcny', project: private_project)
create(:environment, name: 'gprivprd', project: public_project_with_private_environments)
create(:environment, name: 'gprivstg', project: public_project_with_private_environments)
public_project_with_private_environments.update!(namespace: group)
public_project_with_private_environments.project_feature.update!(environments_access_level: Featurable::PRIVATE)
end
context 'using a group' do
@ -23,7 +29,7 @@ RSpec.describe Environments::EnvironmentNamesFinder do
names = described_class.new(group, user).execute
expect(names).to eq(%w[gcny gprd gstg])
expect(names).to eq(%w[gcny gprd gprivprd gprivstg gstg])
end
end
@ -33,7 +39,7 @@ RSpec.describe Environments::EnvironmentNamesFinder do
names = described_class.new(group, user).execute
expect(names).to eq(%w[gcny gprd gstg])
expect(names).to eq(%w[gcny gprd gprivprd gprivstg gstg])
end
end
@ -57,8 +63,18 @@ RSpec.describe Environments::EnvironmentNamesFinder do
end
end
context 'with a public project reporter which has private environments' do
it 'returns environment names for public projects' do
public_project_with_private_environments.add_reporter(user)
names = described_class.new(group, user).execute
expect(names).to eq(%w[gprd gprivprd gprivstg gstg])
end
end
context 'with a group guest' do
it 'returns environment names for all public projects' do
it 'returns environment names for public projects' do
group.add_guest(user)
names = described_class.new(group, user).execute
@ -68,7 +84,7 @@ RSpec.describe Environments::EnvironmentNamesFinder do
end
context 'with a non-member' do
it 'returns environment names for all public projects' do
it 'returns environment names for only public projects with public environments' do
names = described_class.new(group, user).execute
expect(names).to eq(%w[gprd gstg])
@ -76,7 +92,7 @@ RSpec.describe Environments::EnvironmentNamesFinder do
end
context 'without a user' do
it 'returns environment names for all public projects' do
it 'returns environment names for only public projects with public environments' do
names = described_class.new(group).execute
expect(names).to eq(%w[gprd gstg])

View File

@ -106,6 +106,26 @@ RSpec.describe NotesFinder do
end
end
context 'for notes on public issue in public project' do
let_it_be(:public_project) { create(:project, :public) }
let_it_be(:guest_member) { create(:user) }
let_it_be(:reporter_member) { create(:user) }
let_it_be(:guest_project_member) { create(:project_member, :guest, user: guest_member, project: public_project) }
let_it_be(:reporter_project_member) { create(:project_member, :reporter, user: reporter_member, project: public_project) }
let_it_be(:internal_note) { create(:note_on_issue, project: public_project, internal: true) }
let_it_be(:public_note) { create(:note_on_issue, project: public_project) }
it 'shows all notes when the current_user has reporter access' do
notes = described_class.new(reporter_member, project: public_project).execute
expect(notes).to contain_exactly internal_note, public_note
end
it 'shows only public notes when the current_user has guest access' do
notes = described_class.new(guest_member, project: public_project).execute
expect(notes).to contain_exactly public_note
end
end
context 'for target type' do
let(:project) { create(:project, :repository) }
let!(:note1) { create :note_on_issue, project: project }

View File

@ -1598,6 +1598,54 @@ describe('DiffsStoreActions', () => {
);
});
describe('rereadNoteHash', () => {
beforeEach(() => {
window.location.hash = 'note_123';
});
it('dispatches setCurrentDiffFileIdFromNote if the hash is a note URL', () => {
window.location.hash = 'note_123';
return testAction(
diffActions.rereadNoteHash,
{},
{},
[],
[{ type: 'setCurrentDiffFileIdFromNote', payload: '123' }],
);
});
it('dispatches fetchFileByFile if the app is in fileByFile mode', () => {
window.location.hash = 'note_123';
return testAction(
diffActions.rereadNoteHash,
{},
{ viewDiffsFileByFile: true },
[],
[{ type: 'setCurrentDiffFileIdFromNote', payload: '123' }, { type: 'fetchFileByFile' }],
);
});
it('does not try to fetch the diff file if the app is not in fileByFile mode', () => {
window.location.hash = 'note_123';
return testAction(
diffActions.rereadNoteHash,
{},
{ viewDiffsFileByFile: false },
[],
[{ type: 'setCurrentDiffFileIdFromNote', payload: '123' }],
);
});
it('does nothing if the hash is not a note URL', () => {
window.location.hash = 'abcdef1234567890';
return testAction(diffActions.rereadNoteHash, {}, {}, [], []);
});
});
describe('setCurrentDiffFileIdFromNote', () => {
it('commits SET_CURRENT_DIFF_FILE', () => {
const commit = jest.fn();

View File

@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import setWindowLocation from 'helpers/set_window_location_helper';
import * as urlUtility from '~/lib/utils/url_utility';
import AuthorSelect from '~/projects/commits/components/author_select.vue';
import { createStore } from '~/projects/commits/store';
@ -64,39 +65,23 @@ describe('Author Select', () => {
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
describe('user is searching via "filter by commit message"', () => {
it('disables dropdown container', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ hasSearchParam: true });
beforeEach(() => {
setWindowLocation(`?search=foo`);
createComponent();
});
await nextTick();
it('does not disable dropdown container', () => {
expect(findDropdownContainer().attributes('disabled')).toBeUndefined();
});
it('has correct tooltip message', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ hasSearchParam: true });
await nextTick();
it('has correct tooltip message', () => {
expect(findDropdownContainer().attributes('title')).toBe(
'Searching by both author and message is currently not supported.',
);
});
it('disables dropdown', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ hasSearchParam: false });
await nextTick();
expect(findDropdown().attributes('disabled')).toBeUndefined();
});
it('hasSearchParam if user types a truthy string', () => {
wrapper.vm.setSearchParam('false');
expect(wrapper.vm.hasSearchParam).toBe(true);
it('disables dropdown', () => {
expect(findDropdown().attributes('disabled')).toBe('true');
});
});
@ -106,9 +91,8 @@ describe('Author Select', () => {
});
it('displays the current selected author', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ currentAuthor });
setWindowLocation(`?author=${currentAuthor}`);
createComponent();
await nextTick();
expect(findDropdown().attributes('text')).toBe(currentAuthor);
@ -146,12 +130,14 @@ describe('Author Select', () => {
expect(findDropdownItems().at(0).text()).toBe('Any Author');
});
it('displays the project authors', async () => {
await nextTick();
it('displays the project authors', () => {
expect(findDropdownItems()).toHaveLength(authors.length + 1);
});
it('has the correct props', async () => {
setWindowLocation(`?author=${currentAuthor}`);
createComponent();
const [{ avatar_url: avatarUrl, username }] = authors;
const result = {
avatarUrl,
@ -159,16 +145,11 @@ describe('Author Select', () => {
isChecked: true,
};
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ currentAuthor });
await nextTick();
expect(findDropdownItems().at(1).props()).toEqual(expect.objectContaining(result));
});
it("display the author's name", async () => {
await nextTick();
it("display the author's name", () => {
expect(findDropdownItems().at(1).text()).toBe(currentAuthor);
});

View File

@ -1,5 +1,6 @@
import { generateRefDestinationPath } from '~/repository/utils/ref_switcher_utils';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'spec/test_constants';
import { refWithSpecialCharMock, encodedRefWithSpecialCharMock } from '../mock_data';
const projectRootPath = 'root/Project1';
@ -16,16 +17,38 @@ describe('generateRefDestinationPath', () => {
${`${projectRootPath}/-/blob/${currentRef}/dir1/test.js`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/test.js`}
${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/dir2/test.js`}
${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js#L123`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/dir2/test.js#L123`}
`('generates the correct destination path for $currentPath', ({ currentPath, result }) => {
`('generates the correct destination path for $currentPath', ({ currentPath, result }) => {
setWindowLocation(currentPath);
expect(generateRefDestinationPath(projectRootPath, currentRef, selectedRef)).toBe(result);
expect(generateRefDestinationPath(projectRootPath, currentRef, selectedRef)).toBe(
`${TEST_HOST}/${result}`,
);
});
describe('when using symbolic ref names', () => {
it.each`
currentPath | nextRef | result
${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js#L123`} | ${'someHash'} | ${`${projectRootPath}/-/blob/someHash/dir1/dir2/test.js#L123`}
${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js#L123`} | ${'refs/heads/prefixedByUseSymbolicRefNames'} | ${`${projectRootPath}/-/blob/prefixedByUseSymbolicRefNames/dir1/dir2/test.js?ref_type=heads#L123`}
${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js#L123`} | ${'refs/tags/prefixedByUseSymbolicRefNames'} | ${`${projectRootPath}/-/blob/prefixedByUseSymbolicRefNames/dir1/dir2/test.js?ref_type=tags#L123`}
${`${projectRootPath}/-/tree/${currentRef}/dir1/dir2/test.js#L123`} | ${'refs/heads/prefixedByUseSymbolicRefNames'} | ${`${projectRootPath}/-/tree/prefixedByUseSymbolicRefNames/dir1/dir2/test.js?ref_type=heads#L123`}
${`${projectRootPath}/-/tree/${currentRef}/dir1/dir2/test.js#L123`} | ${'refs/tags/prefixedByUseSymbolicRefNames'} | ${`${projectRootPath}/-/tree/prefixedByUseSymbolicRefNames/dir1/dir2/test.js?ref_type=tags#L123`}
${`${projectRootPath}/-/tree/${currentRef}/dir1/dir2/test.js#L123`} | ${'refs/heads/refs/heads/branchNameContainsPrefix'} | ${`${projectRootPath}/-/tree/refs/heads/branchNameContainsPrefix/dir1/dir2/test.js?ref_type=heads#L123`}
`(
'generates the correct destination path for $currentPath with ref type when it can be extracted',
({ currentPath, result, nextRef }) => {
setWindowLocation(currentPath);
expect(generateRefDestinationPath(projectRootPath, currentRef, nextRef)).toBe(
`${TEST_HOST}/${result}`,
);
},
);
});
it('encodes the selected ref', () => {
const result = `${projectRootPath}/-/tree/${encodedRefWithSpecialCharMock}`;
expect(generateRefDestinationPath(projectRootPath, currentRef, refWithSpecialCharMock)).toBe(
result,
`${TEST_HOST}/${result}`,
);
});
});

View File

@ -26,7 +26,7 @@ RSpec.describe HooksHelper do
it 'returns proper data' do
expect(subject).to match(
url: project_hook.url,
url_variables: Gitlab::Json.dump([{ key: 'abc' }])
url_variables: Gitlab::Json.dump([{ key: 'abc' }, { key: 'def' }])
)
end
end

View File

@ -202,7 +202,7 @@ RSpec.describe Banzai::Filter::IssuableReferenceExpansionFilter, feature_categor
filter(link, context)
end.count
expect(control_count).to eq 10
expect(control_count).to eq 11
expect do
filter("#{link} #{link2}", context)

View File

@ -0,0 +1,84 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::NullifyLastErrorFromProjectMirrorData, feature_category: :source_code_management do # rubocop:disable Layout/LineLength
it 'nullifies last_error column on all rows' do
namespaces = table(:namespaces)
projects = table(:projects)
project_import_states = table(:project_mirror_data)
group = namespaces.create!(name: 'gitlab', path: 'gitlab-org')
project_namespace_1 = namespaces.create!(name: 'gitlab', path: 'gitlab-org')
project_namespace_2 = namespaces.create!(name: 'gitlab', path: 'gitlab-org')
project_namespace_3 = namespaces.create!(name: 'gitlab', path: 'gitlab-org')
project_1 = projects.create!(
namespace_id: group.id,
project_namespace_id: project_namespace_1.id,
name: 'test1'
)
project_2 = projects.create!(
namespace_id: group.id,
project_namespace_id: project_namespace_2.id,
name: 'test2'
)
project_3 = projects.create!(
namespace_id: group.id,
project_namespace_id: project_namespace_3.id,
name: 'test3'
)
project_import_state_1 = project_import_states.create!(
project_id: project_1.id,
status: 0,
last_update_started_at: 1.hour.ago,
last_update_scheduled_at: 1.hour.ago,
last_update_at: 1.hour.ago,
last_successful_update_at: 2.days.ago,
last_error: '13:fetch remote: "fatal: unable to look up user:pass@gitlab.com (port 9418) (nodename nor servname provided, or not known)\n": exit status 128.', # rubocop:disable Layout/LineLength
correlation_id_value: SecureRandom.uuid,
jid: SecureRandom.uuid
)
project_import_states.create!(
project_id: project_2.id,
status: 1,
last_update_started_at: 1.hour.ago,
last_update_scheduled_at: 1.hour.ago,
last_update_at: 1.hour.ago,
last_successful_update_at: nil,
next_execution_timestamp: 1.day.from_now,
last_error: '',
correlation_id_value: SecureRandom.uuid,
jid: SecureRandom.uuid
)
project_import_state_3 = project_import_states.create!(
project_id: project_3.id,
status: 2,
last_update_started_at: 1.hour.ago,
last_update_scheduled_at: 1.hour.ago,
last_update_at: 1.hour.ago,
last_successful_update_at: 1.hour.ago,
next_execution_timestamp: 1.day.from_now,
last_error: nil,
correlation_id_value: SecureRandom.uuid,
jid: SecureRandom.uuid
)
migration = described_class.new(
start_id: project_import_state_1.id,
end_id: project_import_state_3.id,
batch_table: :project_mirror_data,
batch_column: :id,
sub_batch_size: 1,
pause_ms: 0,
connection: ApplicationRecord.connection
)
w_last_error_count = -> { project_import_states.where.not(last_error: nil).count } # rubocop:disable CodeReuse/ActiveRecord
expect { migration.perform }.to change(&w_last_error_count).from(2).to(0)
end
end

View File

@ -711,6 +711,7 @@ project:
- packages
- package_files
- rpm_repository_files
- npm_metadata_caches
- packages_cleanup_policy
- alerting_setting
- project_setting

View File

@ -1171,7 +1171,7 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
end
context 'HTML comment lines' do
subject { described_class::MARKDOWN_HTML_COMMENT_LINE_REGEX }
subject { Gitlab::UntrustedRegexp.new(described_class::MARKDOWN_HTML_COMMENT_LINE_REGEX_UNTRUSTED, multiline: true) }
let(:expected) { [['<!-- an HTML comment -->'], ['<!-- another HTML comment -->']] }
let(:markdown) do
@ -1189,20 +1189,20 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
it { is_expected.to match(%(<!-- single line comment -->)) }
it { is_expected.not_to match(%(<!--\nblock comment\n-->)) }
it { is_expected.not_to match(%(must start in first column <!-- comment -->)) }
it { expect(markdown.scan(subject)).to eq expected }
it { expect(subject.scan(markdown)).to eq expected }
end
context 'HTML comment blocks' do
subject { described_class::MARKDOWN_HTML_COMMENT_BLOCK_REGEX }
subject { Gitlab::UntrustedRegexp.new(described_class::MARKDOWN_HTML_COMMENT_BLOCK_REGEX_UNTRUSTED, multiline: true) }
let(:expected) { %(<!-- the start of an HTML comment\n- [ ] list item commented out\n-->) }
let(:expected) { %(<!-- the start of an HTML comment\n- [ ] list item commented out\nmore text -->) }
let(:markdown) do
<<~MARKDOWN
Regular text
<!-- the start of an HTML comment
- [ ] list item commented out
-->
more text -->
MARKDOWN
end

View File

@ -137,6 +137,38 @@ RSpec.describe Gitlab::UntrustedRegexp do
end
end
describe '#extract_named_group' do
let(:re) { described_class.new('(?P<name>\w+) (?P<age>\d+)|(?P<name_only>\w+)') }
let(:text) { 'Bob 40' }
it 'returns values for both named groups' do
matched = re.scan(text).first
expect(re.extract_named_group(:name, matched)).to eq 'Bob'
expect(re.extract_named_group(:age, matched)).to eq '40'
end
it 'returns nil if there was no match for group' do
matched = re.scan('Bob').first
expect(re.extract_named_group(:name, matched)).to be_nil
expect(re.extract_named_group(:age, matched)).to be_nil
expect(re.extract_named_group(:name_only, matched)).to eq 'Bob'
end
it 'returns nil if match is nil' do
matched = '(?P<age>\d+)'.scan(text).first
expect(re.extract_named_group(:age, matched)).to be_nil
end
it 'raises if name is not a capture group' do
matched = re.scan(text).first
expect { re.extract_named_group(:foo, matched) }.to raise_error('Invalid named capture group: foo')
end
end
describe '#match' do
context 'when there are matches' do
it 'returns a match object' do

View File

@ -10,29 +10,36 @@ RSpec.describe Gitlab::UrlSanitizer do
# We want to try with multi-line content because is how error messages are formatted
described_class.sanitize(%Q{
remote: Not Found
fatal: repository '#{url}' not found
fatal: repository `#{url}` not found
})
end
where(:input, :output) do
'http://user:pass@test.com/root/repoC.git/' | 'http://*****:*****@test.com/root/repoC.git/'
'https://user:pass@test.com/root/repoA.git/' | 'https://*****:*****@test.com/root/repoA.git/'
'ssh://user@host.test/path/to/repo.git' | 'ssh://*****@host.test/path/to/repo.git'
# git protocol does not support authentication but clean any details anyway
'git://user:pass@host.test/path/to/repo.git' | 'git://*****:*****@host.test/path/to/repo.git'
'git://host.test/path/to/repo.git' | 'git://host.test/path/to/repo.git'
# http(s), ssh, git, relative, and schemeless URLs should all be masked correctly
urls = ['http://', 'https://', 'ssh://', 'git://', '//', ''].flat_map do |protocol|
[
["#{protocol}test.com", "#{protocol}test.com"],
["#{protocol}test.com/", "#{protocol}test.com/"],
["#{protocol}test.com/path/to/repo.git", "#{protocol}test.com/path/to/repo.git"],
["#{protocol}user@test.com", "#{protocol}*****@test.com"],
["#{protocol}user:pass@test.com", "#{protocol}*****:*****@test.com"],
["#{protocol}user:@test.com", "#{protocol}*****@test.com"],
["#{protocol}:pass@test.com", "#{protocol}:*****@test.com"]
]
end
# SCP-style URLs are left unmodified
'user@server:project.git' | 'user@server:project.git'
'user:pass@server:project.git' | 'user:pass@server:project.git'
urls << ['user@server:project.git', 'user@server:project.git']
urls << ['user:@server:project.git', 'user:@server:project.git']
urls << [':pass@server:project.git', ':pass@server:project.git']
urls << ['user:pass@server:project.git', 'user:pass@server:project.git']
# return an empty string for invalid URLs
'ssh://' | ''
urls << ['ssh://', '']
end
with_them do
it { expect(sanitize_url(input)).to include("repository '#{output}' not found") }
it { expect(sanitize_url(input)).to include("repository `#{output}` not found") }
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Rouge::Formatters::HTMLGitlab do
RSpec.describe Rouge::Formatters::HTMLGitlab, feature_category: :source_code_management do
describe '#format' do
subject { described_class.format(tokens, **options) }
@ -67,5 +67,24 @@ RSpec.describe Rouge::Formatters::HTMLGitlab do
is_expected.to include(%{<span class="unicode-bidi has-tooltip" data-toggle="tooltip" title="#{message}">}).exactly(4).times
end
end
context 'when space characters and zero-width spaces are used' do
let(:lang) { 'ruby' }
let(:tokens) { lexer.lex(code, continue: false) }
let(:code) do
<<~JS
def\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000hello
JS
end
it 'replaces the space characters with spaces' do
is_expected.to eq(
"<span id=\"LC1\" class=\"line\" lang=\"ruby\">" \
"<span class=\"k\">def</span><span class=\"err\"> </span><span class=\"n\">hello</span>" \
"</span>"
)
end
end
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe AddProjectIdForeignKeyToPackagesNpmMetadataCaches,
feature_category: :package_registry do
let(:table) { described_class::SOURCE_TABLE }
let(:column) { described_class::COLUMN }
let(:foreign_key) { -> { described_class.new.foreign_keys_for(table, column).first } }
it 'creates and drops the foreign key' do
reversible_migration do |migration|
migration.before -> do
expect(foreign_key.call).to be(nil)
end
migration.after -> do
expect(foreign_key.call).to have_attributes(column: column.to_s)
end
end
end
end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe NullifyLastErrorFromProjectMirrorData, feature_category: :source_code_management do
let(:migration) { described_class::MIGRATION }
before do
migrate!
end
describe '#up' do
it 'schedules background jobs for each batch of projects' do
expect(migration).to(
have_scheduled_batched_migration(
table_name: :project_mirror_data,
column_name: :id,
interval: described_class::INTERVAL,
batch_size: described_class::BATCH_SIZE,
sub_batch_size: described_class::SUB_BATCH_SIZE
)
)
end
end
describe '#down' do
before do
schema_migrate_down!
end
it 'deletes all batched migration records' do
expect(migration).not_to have_scheduled_batched_migration
end
end
end

View File

@ -35,11 +35,7 @@ RSpec.describe Taskable, feature_category: :team_planning do
TaskList::Item.new('- [ ]', 'First item'),
TaskList::Item.new('- [x]', 'Second item'),
TaskList::Item.new('* [x]', 'First item'),
TaskList::Item.new('* [ ]', 'Second item'),
TaskList::Item.new('+ [ ]', 'No-break space (U+00A0)'),
TaskList::Item.new('+ []', 'Figure space (U+2007)'),
TaskList::Item.new('+ []', 'Narrow no-break space (U+202F)'),
TaskList::Item.new('+ []', 'Thin space (U+2009)')
TaskList::Item.new('* [ ]', 'Second item')
]
end

View File

@ -242,6 +242,22 @@ RSpec.describe WebHook, feature_category: :integrations do
expect(hook.url_variables).to eq({})
end
it 'resets url variables if url is changed and url variables are appended' do
hook.url = 'http://suspicious.example.com/{abc}/{foo}'
hook.url_variables = hook.url_variables.merge('foo' => 'bar')
expect(hook).not_to be_valid
expect(hook.url_variables).to eq({})
end
it 'resets url variables if url is changed and url variables are removed' do
hook.url = 'http://suspicious.example.com/{abc}'
hook.url_variables = hook.url_variables.except("def")
expect(hook).not_to be_valid
expect(hook.url_variables).to eq({})
end
it 'does not reset url variables if both url and url variables are changed' do
hook.url = 'http://example.com/{one}/{two}'
hook.url_variables = { 'one' => 'foo', 'two' => 'bar' }
@ -249,6 +265,18 @@ RSpec.describe WebHook, feature_category: :integrations do
expect(hook).to be_valid
expect(hook.url_variables).to eq({ 'one' => 'foo', 'two' => 'bar' })
end
context 'without url variables' do
subject(:hook) { build_stubbed(:project_hook, project: project, url: 'http://example.com') }
it 'does not reset url variables' do
hook.url = 'http://example.com/{one}/{two}'
hook.url_variables = { 'one' => 'foo', 'two' => 'bar' }
expect(hook).to be_valid
expect(hook.url_variables).to eq({ 'one' => 'foo', 'two' => 'bar' })
end
end
end
it "only consider these branch filter strategies are valid" do

View File

@ -1773,6 +1773,36 @@ RSpec.describe Issue, feature_category: :team_planning do
it 'raises error when feature is invalid' do
expect { issue.issue_type_supports?(:unkown_feature) }.to raise_error(ArgumentError)
end
context 'when issue_type_uses_work_item_types_table feature flag is disabled' do
before do
stub_feature_flags(issue_type_uses_work_item_types_table: false)
end
it 'uses the issue_type column' do
expect(issue).to receive(:issue_type).and_call_original
expect(issue).not_to receive(:work_item_type).and_call_original
issue.issue_type_supports?(:assignee)
end
end
context 'when issue_type_uses_work_item_types_table feature flag is enabled' do
it 'uses the work_item_types table' do
expect(issue).not_to receive(:issue_type).and_call_original
expect(issue).to receive(:work_item_type).and_call_original
issue.issue_type_supports?(:assignee)
end
context 'when the issue is not persisted' do
it 'uses the default work item type' do
non_persisted_issue = build(:issue)
expect(non_persisted_issue.issue_type_supports?(:assignee)).to be_truthy
end
end
end
end
describe '#supports_time_tracking?' do
@ -1920,4 +1950,10 @@ RSpec.describe Issue, feature_category: :team_planning do
end
end
end
describe '#work_item_type_with_default' do
subject { Issue.new.work_item_type_with_default }
it { is_expected.to eq(WorkItems::Type.default_by_type(::Issue::DEFAULT_ISSUE_TYPE)) }
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Packages::Npm::MetadataCache, type: :model, feature_category: :package_registry do
let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache) }
describe 'relationships' do
it { is_expected.to belong_to(:project).inverse_of(:npm_metadata_caches) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:file) }
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:size) }
describe '#package_name' do
it { is_expected.to validate_presence_of(:package_name) }
it { is_expected.to validate_uniqueness_of(:package_name).scoped_to(:project_id) }
it { is_expected.to allow_value('my.app-11.07.2018').for(:package_name) }
it { is_expected.to allow_value('@group-1/package').for(:package_name) }
it { is_expected.to allow_value('@any-scope/package').for(:package_name) }
it { is_expected.to allow_value('unscoped-package').for(:package_name) }
it { is_expected.not_to allow_value('my(dom$$$ain)com.my-app').for(:package_name) }
it { is_expected.not_to allow_value('@inv@lid-scope/package').for(:package_name) }
it { is_expected.not_to allow_value('@scope/../../package').for(:package_name) }
it { is_expected.not_to allow_value('@scope%2e%2e%fpackage').for(:package_name) }
it { is_expected.not_to allow_value('@scope/sub/package').for(:package_name) }
end
end
end

View File

@ -143,6 +143,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
it { is_expected.to have_many(:package_files).class_name('Packages::PackageFile') }
it { is_expected.to have_many(:rpm_repository_files).class_name('Packages::Rpm::RepositoryFile').inverse_of(:project).dependent(:destroy) }
it { is_expected.to have_many(:debian_distributions).class_name('Packages::Debian::ProjectDistribution').dependent(:destroy) }
it { is_expected.to have_many(:npm_metadata_caches).class_name('Packages::Npm::MetadataCache') }
it { is_expected.to have_one(:packages_cleanup_policy).class_name('Packages::Cleanup::Policy').inverse_of(:project) }
it { is_expected.to have_many(:pipeline_artifacts).dependent(:restrict_with_error) }
it { is_expected.to have_many(:terraform_states).class_name('Terraform::State').inverse_of(:project) }

View File

@ -727,6 +727,39 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
describe 'read_prometheus', feature_category: :metrics do
using RSpec::Parameterized::TableSyntax
before do
project.project_feature.update!(metrics_dashboard_access_level: ProjectFeature::ENABLED)
end
let(:policy) { :read_prometheus }
where(:project_visibility, :role, :allowed) do
:public | :anonymous | false
:public | :guest | false
:public | :reporter | true
:internal | :anonymous | false
:internal | :guest | false
:internal | :reporter | true
:private | :anonymous | false
:private | :guest | false
:private | :reporter | true
end
with_them do
let(:current_user) { public_send(role) }
let(:project) { public_send("#{project_visibility}_project") }
if params[:allowed]
it { is_expected.to be_allowed(policy) }
else
it { is_expected.not_to be_allowed(policy) }
end
end
end
describe 'update_max_artifacts_size' do
context 'when no user' do
let(:current_user) { anonymous }
@ -1002,7 +1035,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
let(:current_user) { guest }
it { is_expected.to be_allowed(:metrics_dashboard) }
it { is_expected.to be_allowed(:read_prometheus) }
it { is_expected.to be_disallowed(:read_prometheus) }
it { is_expected.to be_allowed(:read_deployment) }
it { is_expected.to be_allowed(:read_metrics_user_starred_dashboard) }
it { is_expected.to be_allowed(:create_metrics_user_starred_dashboard) }
@ -1012,7 +1045,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
let(:current_user) { anonymous }
it { is_expected.to be_allowed(:metrics_dashboard) }
it { is_expected.to be_allowed(:read_prometheus) }
it { is_expected.to be_disallowed(:read_prometheus) }
it { is_expected.to be_allowed(:read_deployment) }
it { is_expected.to be_disallowed(:read_metrics_user_starred_dashboard) }
it { is_expected.to be_disallowed(:create_metrics_user_starred_dashboard) }
@ -1038,12 +1071,14 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
let(:current_user) { guest }
it { is_expected.to be_disallowed(:metrics_dashboard) }
it { is_expected.to be_disallowed(:read_prometheus) }
end
context 'with anonymous' do
let(:current_user) { anonymous }
it { is_expected.to be_disallowed(:metrics_dashboard) }
it { is_expected.to be_disallowed(:read_prometheus) }
end
end
@ -1066,7 +1101,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
let(:current_user) { guest }
it { is_expected.to be_allowed(:metrics_dashboard) }
it { is_expected.to be_allowed(:read_prometheus) }
it { is_expected.to be_disallowed(:read_prometheus) }
it { is_expected.to be_allowed(:read_deployment) }
it { is_expected.to be_allowed(:read_metrics_user_starred_dashboard) }
it { is_expected.to be_allowed(:create_metrics_user_starred_dashboard) }
@ -1076,6 +1111,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
let(:current_user) { anonymous }
it { is_expected.to be_disallowed(:metrics_dashboard) }
it { is_expected.to be_disallowed(:read_prometheus) }
end
end
end
@ -1098,12 +1134,14 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
let(:current_user) { guest }
it { is_expected.to be_disallowed(:metrics_dashboard) }
it { is_expected.to be_disallowed(:read_prometheus) }
end
context 'with anonymous' do
let(:current_user) { anonymous }
it { is_expected.to be_disallowed(:metrics_dashboard) }
it { is_expected.to be_disallowed(:read_prometheus) }
end
end
@ -1122,12 +1160,14 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
let(:current_user) { guest }
it { is_expected.to be_disallowed(:metrics_dashboard) }
it { is_expected.to be_disallowed(:read_prometheus) }
end
context 'with anonymous' do
let(:current_user) { anonymous }
it { is_expected.to be_disallowed(:metrics_dashboard) }
it { is_expected.to be_disallowed(:read_prometheus) }
end
end
end
@ -2068,7 +2108,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
:public | ProjectFeature::ENABLED | :anonymous | true
:public | ProjectFeature::PRIVATE | :maintainer | true
:public | ProjectFeature::PRIVATE | :developer | true
:public | ProjectFeature::PRIVATE | :guest | true
:public | ProjectFeature::PRIVATE | :guest | false
:public | ProjectFeature::PRIVATE | :anonymous | false
:public | ProjectFeature::DISABLED | :maintainer | false
:public | ProjectFeature::DISABLED | :developer | false
@ -2080,7 +2120,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
:internal | ProjectFeature::ENABLED | :anonymous | false
:internal | ProjectFeature::PRIVATE | :maintainer | true
:internal | ProjectFeature::PRIVATE | :developer | true
:internal | ProjectFeature::PRIVATE | :guest | true
:internal | ProjectFeature::PRIVATE | :guest | false
:internal | ProjectFeature::PRIVATE | :anonymous | false
:internal | ProjectFeature::DISABLED | :maintainer | false
:internal | ProjectFeature::DISABLED | :developer | false

View File

@ -572,6 +572,22 @@ RSpec.describe API::Repositories, feature_category: :source_code_management do
context 'when authenticated', 'as a developer' do
it_behaves_like 'repository compare' do
let(:current_user) { user }
context 'when user does not have read access to the parent project' do
let_it_be(:group) { create(:group) }
let(:forked_project) { fork_project(project, current_user, repository: true, namespace: group) }
before do
forked_project.add_guest(current_user)
end
it 'returns 403 error' do
get api(route, current_user), params: { from: 'improve/awesome', to: 'feature', from_project_id: forked_project.id }
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['message']).to eq("403 Forbidden - You don't have access to this fork's parent project")
end
end
end
end

View File

@ -861,6 +861,21 @@ RSpec.describe MergeRequests::PushOptionsHandlerService, feature_category: :sour
end
end
describe 'when user does not have access to target project' do
let(:push_options) { { create: true, target: 'my-branch' } }
let(:changes) { default_branch_changes }
before do
allow(user1).to receive(:can?).with(:read_code, project).and_return(false)
end
it 'records an error', :sidekiq_inline do
service.execute
expect(service.errors).to eq(["User access was denied"])
end
end
describe 'when MRs are not enabled' do
let(:project) { create(:project, :public, :repository).tap { |pr| pr.add_developer(user1) } }
let(:push_options) { { create: true } }

View File

@ -130,8 +130,8 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
context 'there is userinfo' do
before do
project_hook.update!(
url: 'http://{one}:{two}@example.com',
url_variables: { 'one' => 'a', 'two' => 'b' }
url: 'http://{foo}:{bar}@example.com',
url_variables: { 'foo' => 'a', 'bar' => 'b' }
)
stub_full_request('http://example.com', method: :post)
end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'explore/projects/page_out_of_bounds.html.haml', feature_category: :projects do
let(:page_limit) { 10 }
let(:unsafe_param) { 'hacked_using_unsafe_param!' }
before do
assign(:max_page_number, page_limit)
controller.params[:action] = 'index'
controller.params[:host] = unsafe_param
controller.params[:protocol] = unsafe_param
controller.params[:sort] = 'name_asc'
end
it 'removes unsafe params from the link' do
render
href = "/explore/projects?page=#{page_limit}&sort=name_asc"
button_text = format(_("Back to page %{number}"), number: page_limit)
expect(rendered).to have_link(button_text, href: href)
expect(rendered).not_to include(unsafe_param)
end
end