Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
77cf68da37
commit
a97f1426db
45
CHANGELOG.md
45
CHANGELOG.md
|
|
@ -2,6 +2,21 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 13.5.2 (2020-11-02)
|
||||
|
||||
### Security (9 changes)
|
||||
|
||||
- Add CSRF protection to runner pause and resume. !1021
|
||||
- Do not expose Terraform state record in API.
|
||||
- Path traversal to RCE via LFS upload.
|
||||
- Update container_repository_name_regex to prevent catastrophic backtracking.
|
||||
- Validate nuget package names.
|
||||
- Prevent private repo from being accessed via internal Kubernetes API.
|
||||
- Validate each upload param key in multipart.rb.
|
||||
- Fix XSS vulnerability for job build dependencies.
|
||||
- Fix unauthorized user is able to access schedule pipeline variables and values.
|
||||
|
||||
|
||||
## 13.5.1 (2020-10-22)
|
||||
|
||||
### Other (1 change)
|
||||
|
|
@ -583,6 +598,21 @@ entry.
|
|||
- Bump cluster applications CI template. !45472
|
||||
|
||||
|
||||
## 13.4.5 (2020-11-02)
|
||||
|
||||
### Security (9 changes)
|
||||
|
||||
- Add CSRF protection to runner pause and resume. !1021
|
||||
- Do not expose Terraform state record in API.
|
||||
- Path traversal to RCE via LFS upload.
|
||||
- Update container_repository_name_regex to prevent catastrophic backtracking.
|
||||
- Validate nuget package names.
|
||||
- Prevent private repo from being accessed via internal Kubernetes API.
|
||||
- Validate each upload param key in multipart.rb.
|
||||
- Fix XSS vulnerability for job build dependencies.
|
||||
- Fix unauthorized user is able to access schedule pipeline variables and values.
|
||||
|
||||
|
||||
## 13.4.4 (2020-10-15)
|
||||
|
||||
### Fixed (2 changes)
|
||||
|
|
@ -1241,6 +1271,21 @@ entry.
|
|||
- Expand the visible highlight for collapsed diffs (re: !41393). !42343
|
||||
|
||||
|
||||
## 13.3.9 (2020-11-02)
|
||||
|
||||
### Security (9 changes)
|
||||
|
||||
- Add CSRF protection to runner pause and resume. !1021
|
||||
- Do not expose Terraform state record in API.
|
||||
- Path traversal to RCE via LFS upload.
|
||||
- Update container_repository_name_regex to prevent catastrophic backtracking.
|
||||
- Validate nuget package names.
|
||||
- Prevent private repo from being accessed via internal Kubernetes API.
|
||||
- Validate each upload param key in multipart.rb.
|
||||
- Fix XSS vulnerability for job build dependencies.
|
||||
- Fix unauthorized user is able to access schedule pipeline variables and values.
|
||||
|
||||
|
||||
## 13.3.8 (2020-10-21)
|
||||
|
||||
### Fixed (2 changes)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
8.52.0
|
||||
8.53.0
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
<script>
|
||||
/* eslint-disable vue/no-v-html */
|
||||
import { throttle, isEmpty } from 'lodash';
|
||||
import { mapGetters, mapState, mapActions } from 'vuex';
|
||||
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
|
||||
import { GlLoadingIcon, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
|
||||
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
|
||||
import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
|
||||
import { polyfillSticky } from '~/lib/utils/sticky';
|
||||
|
|
@ -36,6 +35,9 @@ export default {
|
|||
GlLoadingIcon,
|
||||
SharedRunner: () => import('ee_component/jobs/components/shared_runner_limit_block.vue'),
|
||||
},
|
||||
directives: {
|
||||
SafeHtml,
|
||||
},
|
||||
mixins: [delayedJobMixin],
|
||||
props: {
|
||||
artifactHelpUrl: {
|
||||
|
|
@ -223,7 +225,7 @@ export default {
|
|||
</div>
|
||||
|
||||
<callout v-if="shouldRenderHeaderCallout">
|
||||
<div v-html="job.callout_message"></div>
|
||||
<div v-safe-html="job.callout_message"></div>
|
||||
</callout>
|
||||
</header>
|
||||
<!-- EO Header Section -->
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<script>
|
||||
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||
import { formatTime } from '~/lib/utils/datetime_utility';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
|
|
@ -27,7 +26,24 @@ export default {
|
|||
return this.finishedTime !== '';
|
||||
},
|
||||
durationFormatted() {
|
||||
return formatTime(this.duration);
|
||||
const date = new Date(this.duration * 1000);
|
||||
|
||||
let hh = date.getUTCHours();
|
||||
let mm = date.getUTCMinutes();
|
||||
let ss = date.getSeconds();
|
||||
|
||||
// left pad
|
||||
if (hh < 10) {
|
||||
hh = `0${hh}`;
|
||||
}
|
||||
if (mm < 10) {
|
||||
mm = `0${mm}`;
|
||||
}
|
||||
if (ss < 10) {
|
||||
ss = `0${ss}`;
|
||||
}
|
||||
|
||||
return `${hh}:${mm}:${ss}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -54,9 +54,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="gl-display-flex gl-flex-direction-column gl-flex-shrink-0 gl-mr-6 gl-mb-5 js-issues-container"
|
||||
>
|
||||
<div class="gl-display-flex gl-flex-direction-column gl-flex-shrink-0 gl-mr-6 gl-mb-5">
|
||||
<span class="gl-mb-2">
|
||||
{{ label }}
|
||||
<gl-badge variant="muted" size="sm">{{ total }}</gl-badge>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import { GlProgressBar, GlLink, GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { sum } from 'lodash';
|
||||
import { __, n__, sprintf } from '~/locale';
|
||||
import { MAX_MILESTONES_TO_DISPLAY } from '../constants';
|
||||
import IssuableStats from './issuable_stats.vue';
|
||||
|
|
@ -31,6 +30,21 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
openMergeRequestsPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
mergedMergeRequestsPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
closedMergeRequestsPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -45,17 +59,45 @@ export default {
|
|||
});
|
||||
},
|
||||
percentComplete() {
|
||||
const percent = Math.round((this.closedIssuesCount / this.totalIssuesCount) * 100);
|
||||
const percent = Math.round((this.issueCounts.closed / this.issueCounts.total) * 100);
|
||||
return Number.isNaN(percent) ? 0 : percent;
|
||||
},
|
||||
allIssueStats() {
|
||||
return this.milestones.map(m => m.issueStats || {});
|
||||
issueCounts() {
|
||||
return this.milestones
|
||||
.map(m => m.issueStats || {})
|
||||
.reduce(
|
||||
(acc, current) => {
|
||||
acc.total += current.total || 0;
|
||||
acc.closed += current.closed || 0;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
total: 0,
|
||||
closed: 0,
|
||||
},
|
||||
);
|
||||
},
|
||||
totalIssuesCount() {
|
||||
return sum(this.allIssueStats.map(stats => stats.total || 0));
|
||||
showMergeRequestStats() {
|
||||
return this.milestones.some(m => m.mrStats);
|
||||
},
|
||||
closedIssuesCount() {
|
||||
return sum(this.allIssueStats.map(stats => stats.closed || 0));
|
||||
mergeRequestCounts() {
|
||||
return this.milestones
|
||||
.map(m => m.mrStats || {})
|
||||
.reduce(
|
||||
(acc, current) => {
|
||||
acc.total += current.total || 0;
|
||||
acc.merged += current.merged || 0;
|
||||
acc.closed += current.closed || 0;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
total: 0,
|
||||
merged: 0,
|
||||
closed: 0,
|
||||
},
|
||||
);
|
||||
},
|
||||
milestoneLabelText() {
|
||||
return n__('Milestone', 'Milestones', this.milestones.length);
|
||||
|
|
@ -98,7 +140,7 @@ export default {
|
|||
>
|
||||
<span class="gl-mb-3">{{ percentCompleteText }}</span>
|
||||
<span class="gl-w-full">
|
||||
<gl-progress-bar :value="closedIssuesCount" :max="totalIssuesCount" variant="success" />
|
||||
<gl-progress-bar :value="issueCounts.closed" :max="issueCounts.total" variant="success" />
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -129,10 +171,22 @@ export default {
|
|||
</div>
|
||||
<issuable-stats
|
||||
:label="__('Issues')"
|
||||
:total="totalIssuesCount"
|
||||
:closed="closedIssuesCount"
|
||||
:total="issueCounts.total"
|
||||
:closed="issueCounts.closed"
|
||||
:open-path="openIssuesPath"
|
||||
:closed-path="closedIssuesPath"
|
||||
data-testid="issue-stats"
|
||||
/>
|
||||
<issuable-stats
|
||||
v-if="showMergeRequestStats"
|
||||
:label="__('Merge Requests')"
|
||||
:total="mergeRequestCounts.total"
|
||||
:merged="mergeRequestCounts.merged"
|
||||
:closed="mergeRequestCounts.closed"
|
||||
:open-path="openMergeRequestsPath"
|
||||
:merged-path="mergedMergeRequestsPath"
|
||||
:closed-path="closedMergeRequestsPath"
|
||||
data-testid="merge-request-stats"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ module Projects
|
|||
|
||||
redirect_to namespace_project_settings_access_tokens_path, notice: _("Your new project access token has been created.")
|
||||
else
|
||||
render :index
|
||||
redirect_to namespace_project_settings_access_tokens_path, alert: _("Failed to create new project access token: %{token_response_message}") % { token_response_message: token_response.message }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ class Packages::Package < ApplicationRecord
|
|||
validate :package_already_taken, if: :npm?
|
||||
validates :name, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan?
|
||||
validates :name, format: { with: Gitlab::Regex.generic_package_name_regex }, if: :generic?
|
||||
validates :name, format: { with: Gitlab::Regex.nuget_package_name_regex }, if: :nuget?
|
||||
validates :version, format: { with: Gitlab::Regex.nuget_version_regex }, if: :nuget?
|
||||
validates :version, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan?
|
||||
validates :version, format: { with: Gitlab::Regex.maven_version_regex }, if: -> { version? && maven? }
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ module Ci
|
|||
rule { can?(:admin_pipeline) | (can?(:update_build) & owner_of_schedule) }.policy do
|
||||
enable :update_pipeline_schedule
|
||||
enable :admin_pipeline_schedule
|
||||
enable :read_pipeline_schedule_variables
|
||||
end
|
||||
|
||||
rule { can?(:admin_pipeline_schedule) & ~owner_of_schedule }.policy do
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ class BuildDetailsEntity < JobEntity
|
|||
docs_url = "https://docs.gitlab.com/ee/ci/yaml/README.html#dependencies"
|
||||
|
||||
[
|
||||
failure_message.html_safe,
|
||||
failure_message,
|
||||
help_message(docs_url).html_safe
|
||||
].join("<br />")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ module Packages
|
|||
)
|
||||
end
|
||||
end
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
raise InvalidMetadataError.new(e.message)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -79,11 +79,7 @@ module Projects
|
|||
# Directories on disk
|
||||
move_project_folders(project)
|
||||
|
||||
# Move missing group labels to project
|
||||
Labels::TransferService.new(current_user, @old_group, project).execute
|
||||
|
||||
# Move missing group milestones
|
||||
Milestones::TransferService.new(current_user, @old_group, project).execute
|
||||
transfer_missing_group_resources(@old_group)
|
||||
|
||||
# Move uploads
|
||||
move_project_uploads(project)
|
||||
|
|
@ -107,6 +103,12 @@ module Projects
|
|||
refresh_permissions
|
||||
end
|
||||
|
||||
def transfer_missing_group_resources(group)
|
||||
Labels::TransferService.new(current_user, group, project).execute
|
||||
|
||||
Milestones::TransferService.new(current_user, group, project).execute
|
||||
end
|
||||
|
||||
def allowed_transfer?(current_user, project)
|
||||
@new_namespace &&
|
||||
can?(current_user, :change_namespace, project) &&
|
||||
|
|
|
|||
|
|
@ -15,13 +15,20 @@ module ResourceAccessTokens
|
|||
user = create_user
|
||||
|
||||
return error(user.errors.full_messages.to_sentence) unless user.persisted?
|
||||
return error("Failed to provide maintainer access") unless provision_access(resource, user)
|
||||
|
||||
member = create_membership(resource, user)
|
||||
|
||||
unless member.persisted?
|
||||
delete_failed_user(user)
|
||||
return error("Could not provision maintainer access to project access token")
|
||||
end
|
||||
|
||||
token_response = create_personal_access_token(user)
|
||||
|
||||
if token_response.success?
|
||||
success(token_response.payload[:personal_access_token])
|
||||
else
|
||||
delete_failed_user(user)
|
||||
error(token_response.message)
|
||||
end
|
||||
end
|
||||
|
|
@ -43,6 +50,10 @@ module ResourceAccessTokens
|
|||
Users::CreateService.new(current_user, default_user_params).execute(skip_authorization: true)
|
||||
end
|
||||
|
||||
def delete_failed_user(user)
|
||||
DeleteUserWorker.perform_async(current_user.id, user.id, hard_delete: true, skip_authorization: true)
|
||||
end
|
||||
|
||||
def default_user_params
|
||||
{
|
||||
name: params[:name] || "#{resource.name.to_s.humanize} bot",
|
||||
|
|
@ -88,7 +99,7 @@ module ResourceAccessTokens
|
|||
Gitlab::Auth.resource_bot_scopes
|
||||
end
|
||||
|
||||
def provision_access(resource, user)
|
||||
def create_membership(resource, user)
|
||||
resource.add_user(user, :maintainer, expires_at: params[:expires_at])
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ module Terraform
|
|||
|
||||
state.save! unless state.destroyed?
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def lock!
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ class GitlabUploader < CarrierWave::Uploader::Base
|
|||
|
||||
class_attribute :options
|
||||
|
||||
PROTECTED_METHODS = %i(filename cache_dir work_dir store_dir).freeze
|
||||
|
||||
ObjectNotReadyError = Class.new(StandardError)
|
||||
|
||||
class << self
|
||||
# DSL setter
|
||||
def storage_options(options)
|
||||
|
|
@ -33,6 +37,8 @@ class GitlabUploader < CarrierWave::Uploader::Base
|
|||
|
||||
delegate :base_dir, :file_storage?, to: :class
|
||||
|
||||
before :cache, :protect_from_path_traversal!
|
||||
|
||||
def initialize(model, mounted_as = nil, **uploader_context)
|
||||
super(model, mounted_as)
|
||||
end
|
||||
|
|
@ -121,6 +127,9 @@ class GitlabUploader < CarrierWave::Uploader::Base
|
|||
# For example, `FileUploader` builds the storage path based on the associated
|
||||
# project model's `path_with_namespace` value, which can change when the
|
||||
# project or its containing namespace is moved or renamed.
|
||||
#
|
||||
# When implementing this method, raise `ObjectNotReadyError` if the model
|
||||
# does not yet exist, as it will be tested in `#protect_from_path_traversal!`
|
||||
def dynamic_segment
|
||||
raise(NotImplementedError)
|
||||
end
|
||||
|
|
@ -138,4 +147,21 @@ class GitlabUploader < CarrierWave::Uploader::Base
|
|||
def pathname
|
||||
@pathname ||= Pathname.new(path)
|
||||
end
|
||||
|
||||
# Protect against path traversal attacks
|
||||
# This takes a list of methods to test for path traversal, e.g. ../../
|
||||
# and checks each of them. This uses `.send` so that any potential errors
|
||||
# don't block the entire set from being tested.
|
||||
#
|
||||
# @param [CarrierWave::SanitizedFile]
|
||||
# @return [Nil]
|
||||
# @raise [Gitlab::Utils::PathTraversalAttackError]
|
||||
def protect_from_path_traversal!(file)
|
||||
PROTECTED_METHODS.each do |method|
|
||||
Gitlab::Utils.check_path_traversal!(self.send(method)) # rubocop: disable GitlabSecurity/PublicSend
|
||||
|
||||
rescue ObjectNotReadyError
|
||||
# Do nothing. This test was attempted before the file was ready for that method
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ class JobArtifactUploader < GitlabUploader
|
|||
extend Workhorse::UploadPath
|
||||
include ObjectStorage::Concern
|
||||
|
||||
ObjectNotReadyError = Class.new(StandardError)
|
||||
UnknownFileLocationError = Class.new(StandardError)
|
||||
|
||||
storage_options Gitlab.config.artifacts
|
||||
|
|
@ -24,7 +23,9 @@ class JobArtifactUploader < GitlabUploader
|
|||
private
|
||||
|
||||
def dynamic_segment
|
||||
raise ObjectNotReadyError, 'JobArtifact is not ready' unless model.id
|
||||
# This now tests model.created_at because it can for some reason be nil in the test suite,
|
||||
# and it's not clear if this is intentional or not
|
||||
raise ObjectNotReadyError, 'JobArtifact is not ready' unless model.id && model.created_at
|
||||
|
||||
if model.hashed_path?
|
||||
hashed_path
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ class Packages::PackageFileUploader < GitlabUploader
|
|||
private
|
||||
|
||||
def dynamic_segment
|
||||
raise ObjectNotReadyError, "Package model not ready" unless model.id
|
||||
|
||||
Gitlab::HashedPath.new('packages', model.package.id, 'files', model.id, root_hash: model.package.project_id)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -69,10 +69,10 @@
|
|||
= sprite_icon('pencil')
|
||||
.btn-group
|
||||
- if runner.active?
|
||||
= link_to [:pause, :admin, runner], method: :get, class: 'gl-button btn btn-default btn-svg has-tooltip', title: _('Pause'), ref: 'tooltip', aria: { label: _('Pause') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
|
||||
= link_to [:pause, :admin, runner], method: :post, class: 'gl-button btn btn-default btn-svg has-tooltip', title: _('Pause'), ref: 'tooltip', aria: { label: _('Pause') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
|
||||
= sprite_icon('pause')
|
||||
- else
|
||||
= link_to [:resume, :admin, runner], method: :get, class: 'gl-button btn btn-default btn-svg has-tooltip gl-px-3', title: _('Resume'), ref: 'tooltip', aria: { label: _('Resume') }, data: { placement: 'top', container: 'body'} do
|
||||
= link_to [:resume, :admin, runner], method: :post, class: 'gl-button btn btn-default btn-svg has-tooltip gl-px-3', title: _('Resume'), ref: 'tooltip', aria: { label: _('Resume') }, data: { placement: 'top', container: 'body'} do
|
||||
= sprite_icon('play')
|
||||
.btn-group
|
||||
= link_to [:admin, runner], method: :delete, class: 'gl-button btn btn-danger has-tooltip', title: _('Remove'), ref: 'tooltip', aria: { label: _('Remove') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
|
||||
.label-name.gl-flex-shrink-0.gl-mt-2.gl-mr-3
|
||||
= render_label(label, tooltip: false)
|
||||
.label-description.gl-flex-grow-1.gl-overflow-hidden
|
||||
.gl-display-flex.gl-align-items-center.gl-flex-wrap.gl-mt-2
|
||||
.description-text.gl-flex-grow-1.gl-overflow-hidden
|
||||
.label-description.gl-overflow-hidden.gl-w-full
|
||||
.gl-display-flex.gl-align-items-stretch.gl-flex-wrap.gl-mt-2
|
||||
.gl-flex-basis-half.gl-flex-grow-1.gl-overflow-hidden.gl-mr-2
|
||||
- if label.description.present?
|
||||
= markdown_field(label, :description)
|
||||
- elsif show_labels_full_path?(@project, @group)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Improve project labels page card layout consistency
|
||||
merge_request: 45311
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Skip GMA and SSO validation when creating project access tokens for project bots
|
||||
merge_request: 46257
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Bump workhorse to 8.53.0
|
||||
merge_request: 46666
|
||||
author:
|
||||
type: other
|
||||
|
|
@ -148,8 +148,8 @@ namespace :admin do
|
|||
|
||||
resources :runners, only: [:index, :show, :update, :destroy] do
|
||||
member do
|
||||
get :resume
|
||||
get :pause
|
||||
post :resume
|
||||
post :pause
|
||||
end
|
||||
|
||||
collection do
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ Please check the ~"product analytics" [guide](https://docs.gitlab.com/ee/develop
|
|||
MSG
|
||||
|
||||
UPDATE_METRICS_DEFINITIONS_MESSAGE = <<~MSG
|
||||
When adding, changing, or updating metrics, please update the [Event dictionary Usage Ping table](https://docs.gitlab.com/ee/development/product_analytics/event_dictionary.html#usage-ping).
|
||||
When adding, changing, or updating metrics, please update the [Event dictionary Usage Ping table](https://about.gitlab.com/handbook/product/product-analytics-guide#event-dictionary).
|
||||
|
||||
MSG
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ The following documentation enables Okta as a SAML provider.
|
|||
|
||||
## Configure the Okta application
|
||||
|
||||
1. On Okta go to the admin section and choose to **Add an App**.
|
||||
The following guidance is based on this Okta article, on adding a [SAML Application with an Okta Developer account](https://support.okta.com/help/s/article/Why-can-t-I-add-a-SAML-Application-with-an-Okta-Developer-account?language=en_US):
|
||||
|
||||
1. On Okta admin section, make sure to select Classic UI view in the top left corner. From there, choose to **Add an App**.
|
||||
1. When the app screen comes up you see another button to **Create an App** and
|
||||
choose SAML 2.0 on the next screen.
|
||||
1. Now, very important, add a logo
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: Plan
|
||||
group: Certify
|
||||
group: Product Planning
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: Plan
|
||||
group: Portfolio Management
|
||||
group: Product Planning
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: Plan
|
||||
group: Portfolio Management
|
||||
group: Product Planning
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -200,17 +200,23 @@ for more information.
|
|||
|
||||
### Merge Trains feature flag **(PREMIUM ONLY)**
|
||||
|
||||
To enable and disable the Merge Trains feature, use the `:disable_merge_trains` feature flag.
|
||||
Merge trains are automatically enabled when [pipelines for merged results](../index.md#pipelines-for-merged-results)
|
||||
are enabled. To use pipelines for merged results without using merge trains, you must
|
||||
enable a [feature flag](../../../../user/feature_flags.md) that blocks the merge trains
|
||||
feature.
|
||||
|
||||
To check if the feature flag is enabled on your GitLab instance,
|
||||
ask an administrator to execute the following commands:
|
||||
[GitLab administrators with access to the GitLab Rails console](../../../../administration/feature_flags.md)
|
||||
can enable the feature flag to disable merge trains:
|
||||
|
||||
```shell
|
||||
> sudo gitlab-rails console # Login to Rails console of GitLab instance.
|
||||
> Feature.enabled?(:disable_merge_trains) # Check if it's disabled or not.
|
||||
> Feature.enable(:disable_merge_trains) # Enable Merge Trains.
|
||||
> Feature.disable(:disable_merge_trains) # Disable Merge Trains.
|
||||
```ruby
|
||||
Feature.enable(:disable_merge_trains)
|
||||
```
|
||||
|
||||
When you disable this feature, all existing merge trains are cancelled and
|
||||
After you enable this feature flag, all existing merge trains are cancelled and
|
||||
the **Start/Add to Merge Train** button no longer appears in merge requests.
|
||||
|
||||
To disable the feature flag, and enable merge trains again:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:disable_merge_trains)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ See [database guidelines](database/index.md).
|
|||
|
||||
## Product Analytics guides
|
||||
|
||||
- [Product Analytics guide](product_analytics/index.md)
|
||||
- [Product Analytics guide](https://about.gitlab.com/handbook/product/product-analytics-guide/)
|
||||
- [Usage Ping guide](product_analytics/usage_ping.md)
|
||||
- [Snowplow guide](product_analytics/snowplow.md)
|
||||
|
||||
|
|
|
|||
|
|
@ -1392,6 +1392,62 @@ it 'returns a successful response' do
|
|||
end
|
||||
```
|
||||
|
||||
### Testing tips and tricks
|
||||
|
||||
- Avoid false positives:
|
||||
|
||||
Authenticating a user with the `current_user:` argument for `post_graphql`
|
||||
generates more queries on the first request than on subsequent requests on that
|
||||
same user. If you are testing for N+1 queries using
|
||||
[QueryRecorder](query_recorder.md), use a **different** user for each request.
|
||||
|
||||
The below example shows how a test for avoiding N+1 queries should look:
|
||||
|
||||
```ruby
|
||||
RSpec.describe 'Query.project(fullPath).pipelines' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let(:project) { create(:project) }
|
||||
|
||||
let(:query) do
|
||||
%(
|
||||
{
|
||||
project(fullPath: "#{project.full_path}") {
|
||||
pipelines {
|
||||
nodes {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it 'avoids N+1 queries' do
|
||||
first_user = create(:user)
|
||||
second_user = create(:user)
|
||||
create(:ci_pipeline, project: project)
|
||||
|
||||
control_count = ActiveRecord::QueryRecorder.new do
|
||||
post_graphql(query, current_user: first_user)
|
||||
end
|
||||
|
||||
create(:ci_pipeline, project: project)
|
||||
|
||||
expect do
|
||||
post_graphql(query, current_user: second_user) # use a different user to avoid a false positive from authentication queries
|
||||
end.not_to exceed_query_limit(control_count)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
- Mimic the folder structure of `app/graphql/types`:
|
||||
|
||||
For example, tests for fields on `Types::Ci::PipelineType`
|
||||
in `app/graphql/types/ci/pipeline_type.rb` should live in
|
||||
`spec/requests/api/graphql/ci/pipeline_spec.rb` regardless of the query being
|
||||
used to fetch the pipeline data.
|
||||
|
||||
## Notes about Query flow and GraphQL infrastructure
|
||||
|
||||
GitLab's GraphQL infrastructure can be found in `lib/gitlab/graphql`.
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ the `author` field. GitLab team members **should not**.
|
|||
- Any user-facing change **must** have a changelog entry. This includes both visual changes (regardless of how minor), and changes to the rendered DOM which impact how a screen reader may announce the content.
|
||||
- Any client-facing change to our REST and GraphQL APIs **must** have a changelog entry.
|
||||
- Performance improvements **should** have a changelog entry.
|
||||
- Changes that need to be documented in the Product Analytics [Event Dictionary](product_analytics/event_dictionary.md)
|
||||
- Changes that need to be documented in the Product Analytics [Event Dictionary](https://about.gitlab.com/handbook/product/product-analytics-guide#event-dictionary)
|
||||
also require a changelog entry.
|
||||
- _Any_ contribution from a community member, no matter how small, **may** have
|
||||
a changelog entry regardless of these guidelines if the contributor wants one.
|
||||
|
|
@ -55,7 +55,7 @@ the `author` field. GitLab team members **should not**.
|
|||
- Any docs-only changes **should not** have a changelog entry.
|
||||
- Any change behind a disabled feature flag **should not** have a changelog entry.
|
||||
- Any change behind an enabled feature flag **should** have a changelog entry.
|
||||
- Any change that adds new usage data metrics and changes that needs to be documented in Product Analytics [Event Dictionary](telemetry/event_dictionary.md) **should** have a changelog entry.
|
||||
- Any change that adds new usage data metrics and changes that needs to be documented in Product Analytics [Event Dictionary](https://about.gitlab.com/handbook/product/product-analytics-guide#event-dictionary) **should** have a changelog entry.
|
||||
- A change that [removes a feature flag](feature_flags/development.md) **should** have a changelog entry -
|
||||
only if the feature flag did not default to true already.
|
||||
- A fix for a regression introduced and then fixed in the same release (i.e.,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ A database review is required for:
|
|||
database review.
|
||||
- Changes in usage data metrics that use `count` and `distinct_count`.
|
||||
These metrics could have complex queries over large tables.
|
||||
See the [Product Analytics Guide](product_analytics/usage_ping.md#implementing-usage-ping)
|
||||
See the [Product Analytics Guide](https://about.gitlab.com/handbook/product/product-analytics-guide/)
|
||||
for implementation details.
|
||||
|
||||
A database reviewer is expected to look out for obviously complex
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ addressed.
|
|||
|
||||
To determine whether the experiment is a success or not, we must implement tracking events
|
||||
to acquire data for analyzing. We can send events to Snowplow via either the backend or frontend.
|
||||
Read the [product analytics guide](../product_analytics/index.md) for more details.
|
||||
Read the [product analytics guide](https://about.gitlab.com/handbook/product/product-analytics-guide/) for more details.
|
||||
|
||||
#### Track backend events
|
||||
|
||||
|
|
|
|||
|
|
@ -234,6 +234,23 @@ instead of an `ActiveRecord::Relation`:
|
|||
|
||||
<!-- ### External pagination -->
|
||||
|
||||
### External pagination
|
||||
|
||||
There may be times where you need to return data through the GitLab API that is stored in
|
||||
another system. In these cases you may have to paginate a third-party's API.
|
||||
|
||||
An example of this is with our [Error Tracking](../../operations/error_tracking.md) implementation,
|
||||
where we proxy [Sentry errors](../../operations/error_tracking.md#sentry-error-tracking) through
|
||||
the GitLab API. We do this by calling the Sentry API which enforces its own pagination rules.
|
||||
This means we cannot access the collection within GitLab to perform our own custom pagination.
|
||||
|
||||
For consistency, we manually set the pagination cursors based on values returned by the external API, using `Gitlab::Graphql::ExternallyPaginatedArray.new(previous_cursor, next_cursor, *items)`.
|
||||
|
||||
You can see an example implementation in the following files:
|
||||
|
||||
- [`types/error__tracking/sentry_error_collection_type.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/graphql/types/error_tracking/sentry_error_collection_type.rb) which adds an extension to `field :errors`.
|
||||
- [`resolvers/error_tracking/sentry_errors_resolver.rb`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb) which returns the data from the resolver.
|
||||
|
||||
## Testing
|
||||
|
||||
Any GraphQL field that supports pagination and sorting should be tested
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 101 KiB |
|
|
@ -1,32 +1,5 @@
|
|||
---
|
||||
stage: Growth
|
||||
group: Product Analytics
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
redirect_to: 'https://about.gitlab.com/handbook/product/product-analytics-guide/'
|
||||
---
|
||||
|
||||
# Event Dictionary
|
||||
|
||||
**Note: We've temporarily moved the Event Dictionary to a [Google Sheet](https://docs.google.com/spreadsheets/d/1VzE8R72Px_Y_LlE3Z05LxUlG_dumWe3vl-HeUo70TPw/edit?usp=sharing)**. The previous Markdown table exceeded 600 rows making it difficult to manage. In the future, our intention is to move this back into our docs using a [YAML file](https://gitlab.com/gitlab-org/gitlab-docs/-/issues/823).
|
||||
|
||||
The event dictionary is a single source of truth for the metrics and events we collect for product usage data. The Event Dictionary lists all the metrics and events we track, why we're tracking them, and where they are tracked.
|
||||
|
||||
This is a living document that is updated any time a new event is planned or implemented. It includes the following information.
|
||||
|
||||
- Section, stage, or group
|
||||
- Description
|
||||
- Implementation status
|
||||
- Availability by plan type
|
||||
- Code path
|
||||
|
||||
We're currently focusing our Event Dictionary on [Usage Ping](usage_ping.md). In the future, we will also include [Snowplow](snowplow.md). We currently have an initiative across the entire product organization to complete the [Event Dictionary for Usage Ping](https://gitlab.com/groups/gitlab-org/-/epics/4174).
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Open the Event Dictionary and fill in all the **PM to edit** columns highlighted in yellow.
|
||||
1. Check that all the metrics and events are assigned to the correct section, stage, or group. If a metric is used across many groups, assign it to the stage. If a metric is used across many stages, assign it to the section. If a metric is incorrectly assigned to another section, stage, or group, let the PM know you have reassigned it. If your group has no assigned metrics and events, check that your metrics and events are not incorrectly assigned to another PM.
|
||||
1. Add descriptions of what your metrics and events are tracking. Work with your Engineering team or the Product Analytics team if you need help understanding this.
|
||||
1. Add what plans this metric is available on. Work with your Engineering team or the Product Analytics team if you need help understanding this.
|
||||
|
||||
## Planned metrics and events
|
||||
|
||||
For future metrics and events you plan to track, please add them to the Event Dictionary and note the status as `Planned`, `In Progress`, or `Implemented`. Once you have confirmed the metric has been implemented and have confirmed the metric data is in our data warehouse, change the status to **Data Available**.
|
||||
This document was moved to [another location](https://about.gitlab.com/handbook/product/product-analytics-guide/).
|
||||
|
|
|
|||
|
|
@ -1,182 +1,5 @@
|
|||
---
|
||||
stage: Growth
|
||||
group: Product Analytics
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
redirect_to: 'https://about.gitlab.com/handbook/product/product-analytics-guide/'
|
||||
---
|
||||
|
||||
# Product Analytics Guide
|
||||
|
||||
At GitLab, we collect product usage data for the purpose of helping us build a better product. Data helps GitLab understand which parts of the product need improvement and which features we should build next. Product usage data also helps our team better understand the reasons why people use GitLab. With this knowledge we are able to make better product decisions.
|
||||
|
||||
We encourage users to enable tracking, and we embrace full transparency with our tracking approach so it can be easily understood and trusted.
|
||||
|
||||
By enabling tracking, users can:
|
||||
|
||||
- Contribute back to the wider community.
|
||||
- Help GitLab improve on the product.
|
||||
|
||||
## Our tracking tools
|
||||
|
||||
We use three methods to gather product usage data:
|
||||
|
||||
- [Snowplow](#snowplow)
|
||||
- [Usage Ping](#usage-ping)
|
||||
- [Database import](#database-import)
|
||||
|
||||
### Snowplow
|
||||
|
||||
Snowplow is an enterprise-grade marketing and product analytics platform which helps track the way
|
||||
users engage with our website and application.
|
||||
|
||||
Snowplow consists of two components:
|
||||
|
||||
- [Snowplow JS](https://github.com/snowplow/snowplow/wiki/javascript-tracker) tracks client-side
|
||||
events.
|
||||
- [Snowplow Ruby](https://github.com/snowplow/snowplow/wiki/ruby-tracker) tracks server-side events.
|
||||
|
||||
For more details, read the [Snowplow](snowplow.md) guide.
|
||||
|
||||
### Usage Ping
|
||||
|
||||
Usage Ping is a method for GitLab Inc to collect usage data on a GitLab instance. Usage Ping is primarily composed of row counts for different tables in the instance’s database. By comparing these counts month over month (or week over week), we can get a rough sense for how an instance is using the different features within the product. This high-level data is used to help our product, support, and sales teams.
|
||||
|
||||
For more details, read the [Usage Ping](usage_ping.md) guide.
|
||||
|
||||
### Database import
|
||||
|
||||
Database imports are full imports of data into GitLab's data warehouse. For GitLab.com, the PostgreSQL database is loaded into Snowflake data warehouse every 6 hours. For more details, see the [data team handbook](https://about.gitlab.com/handbook/business-ops/data-team/platform/#extract-and-load).
|
||||
|
||||
## What data can be tracked
|
||||
|
||||
Our different tracking tools allows us to track different types of events. The event types and examples of what data can be tracked are outlined below.
|
||||
|
||||
The availability of event types and their tracking tools varies by segment. For example, on Self-Managed Users, we only have reporting using Database records via Usage Ping.
|
||||
|
||||
| Event Types | SaaS Instance | SaaS Plan | SaaS Group | SaaS Session | SaaS User | SM Instance | SM Plan | SM Group | SM Session | SM User |
|
||||
|----------------------------------------|---------------|-----------|------------|--------------|-----------|-------------|---------|----------|------------|---------|
|
||||
| Snowplow (JS Pageview events) | ✅ | 📅 | 📅 | ✅ | 📅 | 📅 | 📅 | 📅 | 📅 | 📅 |
|
||||
| Snowplow (JS UI events) | ✅ | 📅 | 📅 | ✅ | 📅 | 📅 | 📅 | 📅 | 📅 | 📅 |
|
||||
| Snowplow (Ruby Pageview events) | ✅ | 📅 | 📅 | ✅ | 📅 | 📅 | 📅 | 📅 | 📅 | 📅 |
|
||||
| Snowplow (Ruby CRUD / API events) | ✅ | 📅 | 📅 | ✅ | 📅 | 📅 | 📅 | 📅 | 📅 | 📅 |
|
||||
| Usage Ping (Redis UI counters) | 🔄 | 🔄 | 🔄 | ✖️ | 🔄 | 🔄 | 🔄 | 🔄 | ✖️ | 🔄 |
|
||||
| Usage Ping (Redis Pageview counters) | 🔄 | 🔄 | 🔄 | ✖️ | 🔄 | 🔄 | 🔄 | 🔄 | ✖️ | 🔄 |
|
||||
| Usage Ping (Redis CRUD / API counters) | 🔄 | 🔄 | 🔄 | ✖️ | 🔄 | 🔄 | 🔄 | 🔄 | ✖️ | 🔄 |
|
||||
| Usage Ping (Database counters) | ✅ | 🔄 | 📅 | ✖️ | ✅ | ✅ | ✅ | ✅ | ✖️ | ✅ |
|
||||
| Usage Ping (Instance settings) | ✅ | 🔄 | 📅 | ✖️ | ✅ | ✅ | ✅ | ✅ | ✖️ | ✅ |
|
||||
| Usage Ping (Integration settings) | ✅ | 🔄 | 📅 | ✖️ | ✅ | ✅ | ✅ | ✅ | ✖️ | ✅ |
|
||||
| Database import (Database records) | ✅ | ✅ | ✅ | ✖️ | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | ✖️ |
|
||||
|
||||
[Source file](https://docs.google.com/spreadsheets/d/1e8Afo41Ar8x3JxAXJF3nL83UxVZ3hPIyXdt243VnNuE/edit?usp=sharing)
|
||||
|
||||
**Legend**
|
||||
|
||||
✅ Available, 🔄 In Progress, 📅 Planned, ✖️ Not Possible
|
||||
|
||||
SaaS = GitLab.com. SM = Self-Managed instance
|
||||
|
||||
### Pageview events
|
||||
|
||||
- Number of sessions that visited the /dashboard/groups page
|
||||
|
||||
### UI events
|
||||
|
||||
- Number of sessions that clicked on a button or link
|
||||
- Number of sessions that closed a modal
|
||||
|
||||
UI events are any interface-driven actions from the browser including click data.
|
||||
|
||||
### CRUD or API events
|
||||
|
||||
- Number of Git pushes
|
||||
- Number of GraphQL queries
|
||||
- Number of requests to a Rails action or controller
|
||||
|
||||
These are backend events that include the creation, read, update, deletion of records, and other events that might be triggered from layers other than those available in the interface.
|
||||
|
||||
### Database records
|
||||
|
||||
These are raw database records which can be explored using business intelligence tools like Sisense. The full list of available tables can be found in [structure.sql](https://gitlab.com/gitlab-org/gitlab/-/blob/master/db/structure.sql).
|
||||
|
||||
### Instance settings
|
||||
|
||||
These are settings of your instance such as the instance's Git version and if certain features are enabled such as `container_registry_enabled`.
|
||||
|
||||
### Integration settings
|
||||
|
||||
These are integrations your GitLab instance interacts with such as an [external storage provider](../../administration/static_objects_external_storage.md) or an [external container registry](../../administration/packages/container_registry.md#use-an-external-container-registry-with-gitlab-as-an-auth-endpoint). These services must be able to send data back into a GitLab instance for data to be tracked.
|
||||
|
||||
## Reporting level
|
||||
|
||||
Our reporting levels of aggregate or individual reporting varies by segment. For example, on Self-Managed Users, we can report at an aggregate user level using Usage Ping but not on an Individual user level.
|
||||
|
||||
| Aggregated Reporting | SaaS Instance | SaaS Plan | SaaS Group | SaaS Session | SaaS User | SM Instance | SM Plan | SM Group | SM Session | SM User |
|
||||
|----------------------|---------------|-----------|------------|--------------|-----------|-------------|---------|----------|------------|---------|
|
||||
| Snowplow | ✅ | 📅 | 📅 | ✅ | 📅 | ✅ | 📅 | 📅 | ✅ | 📅 |
|
||||
| Usage Ping | ✅ | 🔄 | 📅 | 📅 | ✅ | ✅ | ✅ | ✅ | 📅 | ✅ |
|
||||
| Database import | ✅ | ✅ | ✅ | ✖️ | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | ✖️ |
|
||||
|
||||
| Identifiable Reporting | SaaS Instance | SaaS Plan | SaaS Group | SaaS Session | SaaS User | SM Instance | SM Plan | SM Group | SM Session | SM User |
|
||||
|------------------------|---------------|-----------|------------|--------------|-----------|-------------|---------|----------|------------|---------|
|
||||
| Snowplow | ✅ | 📅 | 📅 | ✅ | 📅 | ✖️ | ✖️ | ✖️ | ✖️ | ✖️ |
|
||||
| Usage Ping | ✅ | 🔄 | 📅 | ✖️ | ✖️ | ✅ | ✅ | ✖️ | ✖️ | ✖️ |
|
||||
| Database import | ✅ | ✅ | ✅ | ✖️ | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | ✖️ |
|
||||
|
||||
**Legend**
|
||||
|
||||
✅ Available, 🔄 In Progress, 📅 Planned, ✖️ Not Possible
|
||||
|
||||
SaaS = GitLab.com. SM = Self-Managed instance
|
||||
|
||||
## Reporting time period
|
||||
|
||||
Our reporting time periods varies by segment. For example, on Self-Managed Users, we can report all time counts and 28 day counts in Usage Ping.
|
||||
|
||||
| Reporting Time Period | All Time | 28 Days | 7 Days | Daily |
|
||||
|-----------------------|----------|---------|--------|-------|
|
||||
| Snowplow | ✅ | ✅ | ✅ | ✅ |
|
||||
| Usage Ping | ✅ | ✅ | 📅 | ✖️ |
|
||||
| Database import | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
**Legend**
|
||||
|
||||
✅ Available, 🔄 In Progress, 📅 Planned, ✖️ Not Possible
|
||||
|
||||
## Systems overview
|
||||
|
||||
The systems overview is a simplified diagram showing the interactions between GitLab Inc and self-managed instances.
|
||||
|
||||

|
||||
|
||||
[Source file](https://app.diagrams.net/#G13DVpN-XnhWGz9tqReIj8pp1UE4ehk_EC)
|
||||
|
||||
### GitLab Inc
|
||||
|
||||
For Product Analytics purposes, GitLab Inc has three major components:
|
||||
|
||||
1. [Data Infrastructure](https://about.gitlab.com/handbook/business-ops/data-team/platform/infrastructure/): This contains everything managed by our data team including Sisense Dashboards for visualization, Snowflake for Data Warehousing, incoming data sources such as PostgreSQL Pipeline and S3 Bucket, and lastly our data collectors [GitLab.com's Snowplow Collector](https://gitlab.com/gitlab-com/gl-infra/readiness/-/tree/master/library/snowplow/) and GitLab's Versions Application.
|
||||
1. GitLab.com: This is the production GitLab application which is made up of a Client and Server. On the Client or browser side, a Snowplow JS Tracker (Frontend) is used to track client-side events. On the Server or application side, a Snowplow Ruby Tracker (Backend) is used to track server-side events. The server also contains Usage Ping which leverages a PostgreSQL database and a Redis in-memory data store to report on usage data. Lastly, the server also contains System Logs which are generated from running the GitLab application.
|
||||
1. [Monitoring infrastructure](https://about.gitlab.com/handbook/engineering/monitoring/): This is the infrastructure used to ensure GitLab.com is operating smoothly. System Logs are sent from GitLab.com to our monitoring infrastructure and collected by a FluentD collector. From FluentD, logs are either sent to long term Google Cloud Services cold storage via Stackdriver, or, they are sent to our Elastic Cluster via Cloud Pub/Sub which can be explored in real-time using Kibana.
|
||||
|
||||
### Self-managed
|
||||
|
||||
For Product Analytics purposes, self-managed instances have two major components:
|
||||
|
||||
1. Data infrastructure: Having a data infrastructure setup is optional on self-managed instances. If you'd like to collect Snowplow tracking events for your self-managed instance, you can setup your own self-managed Snowplow collector and configure your Snowplow events to point to your own collector.
|
||||
1. GitLab: A self-managed GitLab instance contains all of the same components as GitLab.com mentioned above.
|
||||
|
||||
### Differences between GitLab Inc and Self-managed
|
||||
|
||||
As shown by the orange lines, on GitLab.com Snowplow JS, Snowplow Ruby, Usage Ping, and PostgreSQL database imports all flow into GitLab Inc's data infrastructure. However, on self-managed, only Usage Ping flows into GitLab Inc's data infrastructure.
|
||||
|
||||
As shown by the green lines, on GitLab.com system logs flow into GitLab Inc's monitoring infrastructure. On self-managed, there are no logs sent to GitLab Inc's monitoring infrastructure.
|
||||
|
||||
Note (1): Snowplow JS and Snowplow Ruby are available on self-managed, however, the Snowplow Collector endpoint is set to a self-managed Snowplow Collector which GitLab Inc does not have access to.
|
||||
|
||||
## Additional information
|
||||
|
||||
More useful links:
|
||||
|
||||
- [Product Analytics Direction](https://about.gitlab.com/direction/product-analytics/)
|
||||
- [Data Analysis Process](https://about.gitlab.com/handbook/business-ops/data-team/#data-analysis-process/)
|
||||
- [Data for Product Managers](https://about.gitlab.com/handbook/business-ops/data-team/programs/data-for-product-managers/)
|
||||
- [Data Infrastructure](https://about.gitlab.com/handbook/business-ops/data-team/platform/infrastructure/)
|
||||
This document was moved to [another location](https://about.gitlab.com/handbook/product/product-analytics-guide/).
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ This guide provides an overview of how Snowplow works, and implementation detail
|
|||
|
||||
For more information about Product Analytics, see:
|
||||
|
||||
- [Product Analytics Guide](index.md)
|
||||
- [Product Analytics Guide](https://about.gitlab.com/handbook/product/product-analytics-guide/)
|
||||
- [Usage Ping Guide](usage_ping.md)
|
||||
|
||||
More useful links:
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ This guide describes Usage Ping's purpose and how it's implemented.
|
|||
|
||||
For more information about Product Analytics, see:
|
||||
|
||||
- [Product Analytics Guide](index.md)
|
||||
- [Product Analytics Guide](https://about.gitlab.com/handbook/product/product-analytics-guide/)
|
||||
- [Snowplow Guide](snowplow.md)
|
||||
|
||||
More useful links:
|
||||
|
|
@ -613,7 +613,7 @@ We also use `#database-lab` and [explain.depesz.com](https://explain.depesz.com/
|
|||
|
||||
### 5. Add the metric definition
|
||||
|
||||
When adding, changing, or updating metrics, please update the [Event Dictionary's **Usage Ping** table](event_dictionary.md).
|
||||
When adding, changing, or updating metrics, please update the [Event Dictionary's **Usage Ping** table](https://about.gitlab.com/handbook/product/product-analytics-guide#event-dictionary).
|
||||
|
||||
### 6. Add new metric to Versions Application
|
||||
|
||||
|
|
|
|||
|
|
@ -498,7 +498,29 @@ include:
|
|||
Keep in mind that this approach will eventually stop working when the stable repository is removed,
|
||||
so you must eventually fix your custom chart.
|
||||
|
||||
You can find more information in [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/263778).
|
||||
To fix your custom chart:
|
||||
|
||||
1. In your chart directory, update the `repository` value in your `requirements.yaml` file from :
|
||||
|
||||
```yaml
|
||||
repository: "https://kubernetes-charts.storage.googleapis.com/"
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```yaml
|
||||
repository: "https://charts.helm.sh/stable"
|
||||
```
|
||||
|
||||
1. In your chart directory, run `helm dep update .` using the same Helm major version as Auto DevOps.
|
||||
1. Commit the changes for the `requirements.yaml` file.
|
||||
1. If you previously had a `requirements.lock` file, commit the changes to the file.
|
||||
If you did not previously have a `requirements.lock` file in your chart,
|
||||
you do not need to commit the new one. This file is optional, but when present,
|
||||
it's used to verify the integrity of the downloaded dependencies.
|
||||
|
||||
You can find more information in
|
||||
[issue #263778, "Migrate PostgreSQL from stable Helm repo"](https://gitlab.com/gitlab-org/gitlab/-/issues/263778).
|
||||
|
||||
## Development guides
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
type: reference, howto
|
||||
stage: Plan
|
||||
group: Portfolio Management
|
||||
group: Product Planning
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
type: howto
|
||||
stage: Plan
|
||||
group: Portfolio Management
|
||||
group: Product Planning
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
type: reference
|
||||
stage: Plan
|
||||
group: Portfolio Management
|
||||
group: Product Planning
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
type: reference, howto
|
||||
stage: Plan
|
||||
group: Certify
|
||||
group: Product Planning
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: Plan
|
||||
group: Certify
|
||||
group: Product Planning
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ module API
|
|||
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
|
||||
end
|
||||
get ':id/pipeline_schedules/:pipeline_schedule_id' do
|
||||
present pipeline_schedule, with: Entities::Ci::PipelineScheduleDetails
|
||||
present pipeline_schedule, with: Entities::Ci::PipelineScheduleDetails, user: current_user
|
||||
end
|
||||
|
||||
desc 'Create a new pipeline schedule' do
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ module API
|
|||
module Ci
|
||||
class PipelineScheduleDetails < PipelineSchedule
|
||||
expose :last_pipeline, using: ::API::Entities::Ci::PipelineBasic
|
||||
expose :variables, using: ::API::Entities::Ci::Variable
|
||||
expose :variables,
|
||||
using: ::API::Entities::Ci::Variable,
|
||||
if: ->(schedule, options) { Ability.allowed?(options[:user], :read_pipeline_schedule_variables, schedule) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ module API
|
|||
|
||||
# TODO sort out authorization for real
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/220912
|
||||
if !project || !project.public?
|
||||
unless Ability.allowed?(nil, :download_code, project)
|
||||
not_found!
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ module API
|
|||
|
||||
env['api.format'] = :binary # this bypasses json serialization
|
||||
body state.latest_file.read
|
||||
status :ok
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -55,8 +54,10 @@ module API
|
|||
|
||||
remote_state_handler.handle_with_lock do |state|
|
||||
state.update_file!(CarrierWaveStringFile.new(data), version: params[:serial], build: current_authenticated_job)
|
||||
status :ok
|
||||
end
|
||||
|
||||
body false
|
||||
status :ok
|
||||
end
|
||||
|
||||
desc 'Delete a terraform state of a certain name'
|
||||
|
|
@ -66,8 +67,10 @@ module API
|
|||
|
||||
remote_state_handler.handle_with_lock do |state|
|
||||
state.destroy!
|
||||
status :ok
|
||||
end
|
||||
|
||||
body false
|
||||
status :ok
|
||||
end
|
||||
|
||||
desc 'Lock a terraform state of a certain name'
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ module Gitlab
|
|||
RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS'
|
||||
JWT_PARAM_SUFFIX = '.gitlab-workhorse-upload'
|
||||
JWT_PARAM_FIXED_KEY = 'upload'
|
||||
REWRITTEN_FIELD_NAME_MAX_LENGTH = 10000.freeze
|
||||
|
||||
class Handler
|
||||
def initialize(env, message)
|
||||
|
|
@ -41,6 +42,8 @@ module Gitlab
|
|||
|
||||
def with_open_files
|
||||
@rewritten_fields.each do |field, tmp_path|
|
||||
raise "invalid field: #{field.inspect}" unless valid_field_name?(field)
|
||||
|
||||
parsed_field = Rack::Utils.parse_nested_query(field)
|
||||
raise "unexpected field: #{field.inspect}" unless parsed_field.count == 1
|
||||
|
||||
|
|
@ -108,6 +111,17 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def valid_field_name?(name)
|
||||
# length validation
|
||||
return false if name.size >= REWRITTEN_FIELD_NAME_MAX_LENGTH
|
||||
|
||||
# brackets validation
|
||||
return false if name.include?('[]') || name.start_with?('[', ']')
|
||||
return false unless ::Gitlab::Utils.valid_brackets?(name, allow_nested: false)
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def package_allowed_paths
|
||||
packages_config = ::Gitlab.config.packages
|
||||
return [] unless allow_packages_storage_path?(packages_config)
|
||||
|
|
@ -141,6 +155,8 @@ module Gitlab
|
|||
class HandlerForJWTParams < Handler
|
||||
def with_open_files
|
||||
@rewritten_fields.keys.each do |field|
|
||||
raise "invalid field: #{field.inspect}" unless valid_field_name?(field)
|
||||
|
||||
parsed_field = Rack::Utils.parse_nested_query(field)
|
||||
raise "unexpected field: #{field.inspect}" unless parsed_field.count == 1
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,10 @@ module Gitlab
|
|||
maven_app_name_regex
|
||||
end
|
||||
|
||||
def nuget_package_name_regex
|
||||
@nuget_package_name_regex ||= %r{\A[-+\.\_a-zA-Z0-9]+\z}.freeze
|
||||
end
|
||||
|
||||
def nuget_version_regex
|
||||
@nuget_version_regex ||= /
|
||||
\A#{_semver_major_minor_patch_regex}(\.\d*)?#{_semver_prerelease_build_regex}\z
|
||||
|
|
@ -208,7 +212,7 @@ module Gitlab
|
|||
# See https://github.com/docker/distribution/blob/master/reference/regexp.go.
|
||||
#
|
||||
def container_repository_name_regex
|
||||
@container_repository_regex ||= %r{\A[a-z0-9]+((?:[._/]|__|[-]{0,10})[a-z0-9]+)*\Z}
|
||||
@container_repository_regex ||= %r{\A[a-z0-9]+(([._/]|__|-*)[a-z0-9])*\z}
|
||||
end
|
||||
|
||||
##
|
||||
|
|
|
|||
|
|
@ -174,3 +174,5 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
Gitlab::UsageDataCounters::IssueActivityUniqueCounter.prepend_if_ee('EE::Gitlab::UsageDataCounters::IssueActivityUniqueCounter')
|
||||
|
|
|
|||
|
|
@ -315,3 +315,7 @@
|
|||
category: issues_edit
|
||||
redis_slot: project_management
|
||||
aggregation: daily
|
||||
- name: g_project_management_issue_health_status_changed
|
||||
category: issues_edit
|
||||
redis_slot: project_management
|
||||
aggregation: daily
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ module Gitlab
|
|||
# Also see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24223#note_284122580
|
||||
# It also checks for ALT_SEPARATOR aka '\' (forward slash)
|
||||
def check_path_traversal!(path)
|
||||
return unless path.is_a?(String)
|
||||
|
||||
path = decode_path(path)
|
||||
path_regex = /(\A(\.{1,2})\z|\A\.\.[\/\\]|[\/\\]\.\.\z|[\/\\]\.\.[\/\\]|\n)/
|
||||
|
||||
|
|
@ -208,5 +210,33 @@ module Gitlab
|
|||
def stable_sort_by(list)
|
||||
list.sort_by.with_index { |x, idx| [yield(x), idx] }
|
||||
end
|
||||
|
||||
# Check for valid brackets (`[` and `]`) in a string using this aspects:
|
||||
# * open brackets count == closed brackets count
|
||||
# * (optionally) reject nested brackets via `allow_nested: false`
|
||||
# * open / close brackets coherence, eg. ][[] -> invalid
|
||||
def valid_brackets?(string = '', allow_nested: true)
|
||||
# remove everything except brackets
|
||||
brackets = string.remove(/[^\[\]]/)
|
||||
|
||||
return true if brackets.empty?
|
||||
# balanced counts check
|
||||
return false if brackets.size.odd?
|
||||
|
||||
unless allow_nested
|
||||
# nested brackets check
|
||||
return false if brackets.include?('[[') || brackets.include?(']]')
|
||||
end
|
||||
|
||||
# open / close brackets coherence check
|
||||
untrimmed = brackets
|
||||
loop do
|
||||
trimmed = untrimmed.gsub('[]', '')
|
||||
return true if trimmed.empty?
|
||||
return false if trimmed == untrimmed
|
||||
|
||||
untrimmed = trimmed
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11072,6 +11072,9 @@ msgstr ""
|
|||
msgid "Failed to create import label for jira import."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to create new project access token: %{token_response_message}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to create repository"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Invalid uploads that must be rejected', :api, :js do
|
||||
include_context 'file upload requests helpers'
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:user) { create(:user, :admin) }
|
||||
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
context 'invalid upload key', :capybara_ignore_server_errors do
|
||||
let(:api_path) { "/projects/#{project.id}/packages/nuget/" }
|
||||
let(:url) { capybara_url(api(api_path)) }
|
||||
let(:file) { fixture_file_upload('spec/fixtures/dk.png') }
|
||||
|
||||
subject do
|
||||
HTTParty.put(
|
||||
url,
|
||||
basic_auth: { user: user.username, password: personal_access_token.token },
|
||||
body: body
|
||||
)
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'rejecting invalid keys' do |key_name:, message: nil|
|
||||
context "with invalid key #{key_name}" do
|
||||
let(:body) { { key_name => file, 'package[test][name]' => 'test' } }
|
||||
|
||||
it { expect { subject }.not_to change { Packages::Package.nuget.count } }
|
||||
|
||||
it { expect(subject.code).to eq(500) }
|
||||
|
||||
it { expect(subject.body).to include(message.presence || "invalid field: \"#{key_name}\"") }
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'by rejecting uploads with an invalid key' do
|
||||
it_behaves_like 'rejecting invalid keys', key_name: 'package[test'
|
||||
it_behaves_like 'rejecting invalid keys', key_name: '[]'
|
||||
it_behaves_like 'rejecting invalid keys', key_name: '[package]test'
|
||||
it_behaves_like 'rejecting invalid keys', key_name: 'package][test]]'
|
||||
it_behaves_like 'rejecting invalid keys', key_name: 'package[test[nested]]'
|
||||
end
|
||||
|
||||
# These keys are rejected directly by rack itself.
|
||||
# The request will not be received by multipart.rb (can't use the 'handling file uploads' shared example)
|
||||
it_behaves_like 'rejecting invalid keys', key_name: 'x' * 11000, message: 'Puma caught this error: exceeded available parameter key space (RangeError)'
|
||||
it_behaves_like 'rejecting invalid keys', key_name: 'package[]test', message: 'Puma caught this error: expected Hash (got Array)'
|
||||
|
||||
it_behaves_like 'handling file uploads', 'by rejecting uploads with an invalid key'
|
||||
end
|
||||
end
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`~/releases/components/issuable_stats.vue matches snapshot 1`] = `
|
||||
"<div class=\\"gl-display-flex gl-flex-direction-column gl-flex-shrink-0 gl-mr-6 gl-mb-5 js-issues-container\\"><span class=\\"gl-mb-2\\">
|
||||
"<div class=\\"gl-display-flex gl-flex-direction-column gl-flex-shrink-0 gl-mr-6 gl-mb-5\\"><span class=\\"gl-mb-2\\">
|
||||
Items
|
||||
<span class=\\"badge badge-muted badge-pill gl-badge sm\\"><!----> 10</span></span>
|
||||
<div class=\\"gl-display-flex\\"><span data-testid=\\"open-stat\\" class=\\"gl-white-space-pre-wrap\\">Open: <a href=\\"path/to/open/items\\" class=\\"gl-link\\">1</a></span> <span class=\\"gl-mx-2\\">•</span> <span data-testid=\\"merged-stat\\" class=\\"gl-white-space-pre-wrap\\">Merged: <a href=\\"path/to/merged/items\\" class=\\"gl-link\\">7</a></span> <span class=\\"gl-mx-2\\">•</span> <span data-testid=\\"closed-stat\\" class=\\"gl-white-space-pre-wrap\\">Closed: <a href=\\"path/to/closed/items\\" class=\\"gl-link\\">2</a></span></div>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ describe('Release block milestone info', () => {
|
|||
|
||||
const milestoneProgressBarContainer = () => wrapper.find('.js-milestone-progress-bar-container');
|
||||
const milestoneListContainer = () => wrapper.find('.js-milestone-list-container');
|
||||
const issuesContainer = () => wrapper.find('.js-issues-container');
|
||||
const issuesContainer = () => wrapper.find('[data-testid="issue-stats"]');
|
||||
const mergeRequestsContainer = () => wrapper.find('[data-testid="merge-request-stats"]');
|
||||
|
||||
describe('with default props', () => {
|
||||
beforeEach(() => factory({ milestones }));
|
||||
|
|
@ -187,4 +188,33 @@ describe('Release block milestone info', () => {
|
|||
|
||||
expectAllZeros();
|
||||
});
|
||||
|
||||
describe('if the API response is missing the "mr_stats" property', () => {
|
||||
beforeEach(() => factory({ milestones }));
|
||||
|
||||
it('does not render merge request stats', () => {
|
||||
expect(mergeRequestsContainer().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if the API response includes the "mr_stats" property', () => {
|
||||
beforeEach(() => {
|
||||
milestones = milestones.map(m => ({
|
||||
...m,
|
||||
mrStats: {
|
||||
total: 15,
|
||||
merged: 12,
|
||||
closed: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
return factory({ milestones });
|
||||
});
|
||||
|
||||
it('renders merge request stats', () => {
|
||||
expect(trimText(mergeRequestsContainer().text())).toBe(
|
||||
'Merge Requests 30 Open: 4 • Merged: 24 • Closed: 2',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ RSpec.describe Gitlab::ExclusiveLeaseHelpers, :clean_gitlab_redis_shared_state d
|
|||
let(:options) { { retries: 0 } }
|
||||
|
||||
it 'never sleeps' do
|
||||
expect(class_instance).not_to receive(:sleep)
|
||||
expect_any_instance_of(Gitlab::ExclusiveLeaseHelpers::SleepingLock).not_to receive(:sleep)
|
||||
|
||||
expect { subject }.to raise_error('Failed to obtain a lock')
|
||||
end
|
||||
|
|
@ -98,7 +98,7 @@ RSpec.describe Gitlab::ExclusiveLeaseHelpers, :clean_gitlab_redis_shared_state d
|
|||
let(:options) { { retries: 1, sleep_sec: 0.05.seconds } }
|
||||
|
||||
it 'receives the specified argument' do
|
||||
expect_any_instance_of(Object).to receive(:sleep).with(0.05.seconds).once
|
||||
expect_any_instance_of(Gitlab::ExclusiveLeaseHelpers::SleepingLock).to receive(:sleep).with(0.05.seconds).once
|
||||
|
||||
expect { subject }.to raise_error('Failed to obtain a lock')
|
||||
end
|
||||
|
|
@ -108,8 +108,8 @@ RSpec.describe Gitlab::ExclusiveLeaseHelpers, :clean_gitlab_redis_shared_state d
|
|||
let(:options) { { retries: 2, sleep_sec: ->(num) { 0.1 + num } } }
|
||||
|
||||
it 'receives the specified argument' do
|
||||
expect_any_instance_of(Object).to receive(:sleep).with(1.1.seconds).once
|
||||
expect_any_instance_of(Object).to receive(:sleep).with(2.1.seconds).once
|
||||
expect_any_instance_of(Gitlab::ExclusiveLeaseHelpers::SleepingLock).to receive(:sleep).with(1.1.seconds).once
|
||||
expect_any_instance_of(Gitlab::ExclusiveLeaseHelpers::SleepingLock).to receive(:sleep).with(2.1.seconds).once
|
||||
|
||||
expect { subject }.to raise_error('Failed to obtain a lock')
|
||||
end
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ RSpec.describe Gitlab::ImportExport::AttributesFinder do
|
|||
end
|
||||
|
||||
before do
|
||||
allow_any_instance_of(Gitlab::ImportExport).to receive(:config_file).and_return(test_config)
|
||||
allow(Gitlab::ImportExport).to receive(:config_file).and_return(test_config)
|
||||
end
|
||||
|
||||
it 'generates hash from project tree config' do
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ RSpec.describe Gitlab::ImportExport::Group::LegacyTreeSaver do
|
|||
|
||||
before do
|
||||
group.add_maintainer(user)
|
||||
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
|
||||
allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
|
||||
end
|
||||
|
||||
after do
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ RSpec.describe Gitlab::ImportExport::Importer do
|
|||
subject(:importer) { described_class.new(project) }
|
||||
|
||||
before do
|
||||
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(test_path)
|
||||
allow(Gitlab::ImportExport).to receive(:storage_path).and_return(test_path)
|
||||
allow_any_instance_of(Gitlab::ImportExport::FileImporter).to receive(:remove_import_file)
|
||||
stub_uploads_object_storage(FileUploader)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ RSpec.describe Gitlab::ImportExport::LfsRestorer do
|
|||
subject(:restorer) { described_class.new(project: project, shared: shared) }
|
||||
|
||||
before do
|
||||
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
|
||||
allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
|
||||
FileUtils.mkdir_p(shared.export_path)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -123,15 +123,46 @@ RSpec.describe Gitlab::Middleware::Multipart do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with invalid key in parameters' do
|
||||
context 'with an invalid upload key' do
|
||||
include_context 'with one temporary file for multipart'
|
||||
|
||||
let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) }
|
||||
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'wrong_key', filename: filename, remote_id: remote_id) }
|
||||
RSpec.shared_examples 'rejecting the invalid key' do |key_in_header:, key_in_upload_params:, error_message:|
|
||||
let(:rewritten_fields) { rewritten_fields_hash(key_in_header => uploaded_filepath) }
|
||||
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: key_in_upload_params, filename: filename, remote_id: remote_id) }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject }.to raise_error(RuntimeError, 'Empty JWT param: file.gitlab-workhorse-upload')
|
||||
it 'raises an error' do
|
||||
expect { subject }.to raise_error(RuntimeError, error_message)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'rejecting the invalid key',
|
||||
key_in_header: 'file',
|
||||
key_in_upload_params: 'wrong_key',
|
||||
error_message: 'Empty JWT param: file.gitlab-workhorse-upload'
|
||||
it_behaves_like 'rejecting the invalid key',
|
||||
key_in_header: 'user[avatar',
|
||||
key_in_upload_params: 'user[avatar]',
|
||||
error_message: 'invalid field: "user[avatar"'
|
||||
it_behaves_like 'rejecting the invalid key',
|
||||
key_in_header: '[user]avatar',
|
||||
key_in_upload_params: 'user[avatar]',
|
||||
error_message: 'invalid field: "[user]avatar"'
|
||||
it_behaves_like 'rejecting the invalid key',
|
||||
key_in_header: 'user[]avatar',
|
||||
key_in_upload_params: 'user[avatar]',
|
||||
error_message: 'invalid field: "user[]avatar"'
|
||||
it_behaves_like 'rejecting the invalid key',
|
||||
key_in_header: 'user[avatar[image[url]]]',
|
||||
key_in_upload_params: 'user[avatar]',
|
||||
error_message: 'invalid field: "user[avatar[image[url]]]"'
|
||||
it_behaves_like 'rejecting the invalid key',
|
||||
key_in_header: '[]',
|
||||
key_in_upload_params: 'user[avatar]',
|
||||
error_message: 'invalid field: "[]"'
|
||||
it_behaves_like 'rejecting the invalid key',
|
||||
key_in_header: 'x' * 11000,
|
||||
key_in_upload_params: 'user[avatar]',
|
||||
error_message: "invalid field: \"#{'x' * 11000}\""
|
||||
end
|
||||
|
||||
context 'with a modified JWT payload' do
|
||||
|
|
|
|||
|
|
@ -139,6 +139,58 @@ RSpec.describe Gitlab::Middleware::Multipart do
|
|||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid key in header' do
|
||||
include_context 'with one temporary file for multipart'
|
||||
|
||||
RSpec.shared_examples 'rejecting the invalid key' do |key_in_header:, key_in_upload_params:, error_message:|
|
||||
let(:rewritten_fields) { rewritten_fields_hash(key_in_header => uploaded_filepath) }
|
||||
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: key_in_upload_params, filename: filename, remote_id: remote_id) }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject }.to raise_error(RuntimeError, error_message)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'rejecting the invalid key',
|
||||
key_in_header: 'user[avatar',
|
||||
key_in_upload_params: 'user[avatar]',
|
||||
error_message: 'invalid field: "user[avatar"'
|
||||
it_behaves_like 'rejecting the invalid key',
|
||||
key_in_header: '[user]avatar',
|
||||
key_in_upload_params: 'user[avatar]',
|
||||
error_message: 'invalid field: "[user]avatar"'
|
||||
it_behaves_like 'rejecting the invalid key',
|
||||
key_in_header: 'user[]avatar',
|
||||
key_in_upload_params: 'user[avatar]',
|
||||
error_message: 'invalid field: "user[]avatar"'
|
||||
it_behaves_like 'rejecting the invalid key',
|
||||
key_in_header: 'user[avatar[image[url]]]',
|
||||
key_in_upload_params: 'user[avatar]',
|
||||
error_message: 'invalid field: "user[avatar[image[url]]]"'
|
||||
it_behaves_like 'rejecting the invalid key',
|
||||
key_in_header: '[]',
|
||||
key_in_upload_params: 'user[avatar]',
|
||||
error_message: 'invalid field: "[]"'
|
||||
it_behaves_like 'rejecting the invalid key',
|
||||
key_in_header: 'x' * 11000,
|
||||
key_in_upload_params: 'user[avatar]',
|
||||
error_message: "invalid field: \"#{'x' * 11000}\""
|
||||
end
|
||||
|
||||
context 'with key with unbalanced brackets in header' do
|
||||
include_context 'with one temporary file for multipart'
|
||||
|
||||
let(:invalid_key) { 'user[avatar' }
|
||||
let(:rewritten_fields) { rewritten_fields_hash( invalid_key => uploaded_filepath) }
|
||||
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'user[avatar]', filename: filename, remote_id: remote_id) }
|
||||
|
||||
it 'builds no UploadedFile' do
|
||||
expect(app).not_to receive(:call)
|
||||
|
||||
expect { subject }.to raise_error(RuntimeError, "invalid field: \"#{invalid_key}\"")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -137,11 +137,16 @@ RSpec.describe Gitlab::Regex do
|
|||
it { is_expected.to match('my/awesome/image-1') }
|
||||
it { is_expected.to match('my/awesome/image.test') }
|
||||
it { is_expected.to match('my/awesome/image--test') }
|
||||
# docker distribution allows for infinite `-`
|
||||
# https://github.com/docker/distribution/blob/master/reference/regexp.go#L13
|
||||
# but we have a range of 0,10 to add a reasonable limit.
|
||||
it { is_expected.not_to match('my/image-----------test') }
|
||||
it { is_expected.to match('my/image__test') }
|
||||
# this example tests for catastrophic backtracking
|
||||
it { is_expected.to match('user1/project/a_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb------------x') }
|
||||
it { is_expected.not_to match('user1/project/a_bbbbb-------------') }
|
||||
it { is_expected.not_to match('my/image-.test') }
|
||||
it { is_expected.not_to match('my/image___test') }
|
||||
it { is_expected.not_to match('my/image_.test') }
|
||||
it { is_expected.not_to match('my/image_-test') }
|
||||
it { is_expected.not_to match('my/image..test') }
|
||||
it { is_expected.not_to match('my/image\ntest') }
|
||||
it { is_expected.not_to match('.my/image') }
|
||||
it { is_expected.not_to match('my/image.') }
|
||||
end
|
||||
|
|
@ -372,6 +377,21 @@ RSpec.describe Gitlab::Regex do
|
|||
it { is_expected.not_to match('%2e%2e%2f1.2.3') }
|
||||
end
|
||||
|
||||
describe '.nuget_package_name_regex' do
|
||||
subject { described_class.nuget_package_name_regex }
|
||||
|
||||
it { is_expected.to match('My.Package') }
|
||||
it { is_expected.to match('My.Package.Mvc') }
|
||||
it { is_expected.to match('MyPackage') }
|
||||
it { is_expected.to match('My.23.Package') }
|
||||
it { is_expected.to match('My23Package') }
|
||||
it { is_expected.to match('runtime.my-test64.runtime.package.Mvc') }
|
||||
it { is_expected.to match('my_package') }
|
||||
it { is_expected.not_to match('My/package') }
|
||||
it { is_expected.not_to match('../../../my_package') }
|
||||
it { is_expected.not_to match('%2e%2e%2fmy_package') }
|
||||
end
|
||||
|
||||
describe '.pypi_version_regex' do
|
||||
subject { described_class.pypi_version_regex }
|
||||
|
||||
|
|
|
|||
|
|
@ -8,42 +8,8 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
let(:user3) { build(:user, id: 3) }
|
||||
let(:time) { Time.zone.now }
|
||||
|
||||
shared_examples 'tracks and counts action' do
|
||||
before do
|
||||
stub_application_setting(usage_ping_enabled: true)
|
||||
end
|
||||
|
||||
def count_unique(date_from:, date_to:)
|
||||
Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: action, start_date: date_from, end_date: date_to)
|
||||
end
|
||||
|
||||
specify do
|
||||
aggregate_failures do
|
||||
expect(track_action(author: user1)).to be_truthy
|
||||
expect(track_action(author: user1)).to be_truthy
|
||||
expect(track_action(author: user2)).to be_truthy
|
||||
expect(track_action(author: user3, time: time - 3.days)).to be_truthy
|
||||
|
||||
expect(count_unique(date_from: time, date_to: time)).to eq(2)
|
||||
expect(count_unique(date_from: time - 5.days, date_to: 1.day.since(time))).to eq(3)
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not track edit actions if author is not present' do
|
||||
expect(track_action(author: nil)).to be_nil
|
||||
end
|
||||
|
||||
context 'when feature flag track_issue_activity_actions is disabled' do
|
||||
it 'does not track edit actions' do
|
||||
stub_feature_flags(track_issue_activity_actions: false)
|
||||
|
||||
expect(track_action(author: user1)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for Issue title edit actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_TITLE_CHANGED }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -53,7 +19,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue description edit actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_DESCRIPTION_CHANGED }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -63,7 +29,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue assignee edit actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_ASSIGNEE_CHANGED }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -73,7 +39,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue make confidential actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_MADE_CONFIDENTIAL }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -83,7 +49,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue make visible actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_MADE_VISIBLE }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -93,7 +59,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue created actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_CREATED }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -103,7 +69,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue closed actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_CLOSED }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -113,7 +79,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue reopened actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_REOPENED }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -123,7 +89,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue label changed actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_LABEL_CHANGED }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -133,7 +99,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue cross-referenced actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_CROSS_REFERENCED }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -143,7 +109,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue moved actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_MOVED }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -153,7 +119,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue relate actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_RELATED }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -163,7 +129,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue unrelate actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_UNRELATED }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -173,7 +139,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue marked as duplicate actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_MARKED_AS_DUPLICATE }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -183,7 +149,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue locked actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_LOCKED }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -193,7 +159,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue unlocked actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_UNLOCKED }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -203,7 +169,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue added to epic actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_ADDED_TO_EPIC}
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -213,7 +179,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue removed from epic actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_REMOVED_FROM_EPIC}
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -223,7 +189,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue changed epic actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_CHANGED_EPIC}
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -233,7 +199,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue designs added actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_DESIGNS_ADDED }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -243,7 +209,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue designs modified actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_DESIGNS_MODIFIED }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -253,7 +219,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue designs removed actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_DESIGNS_REMOVED }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -263,7 +229,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue due date changed actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_DUE_DATE_CHANGED }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -273,7 +239,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue time estimate changed actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_TIME_ESTIMATE_CHANGED }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -283,7 +249,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue time spent changed actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_TIME_SPENT_CHANGED }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -293,7 +259,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue comment added actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_COMMENT_ADDED }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -303,7 +269,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue comment edited actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_COMMENT_EDITED }
|
||||
|
||||
def track_action(params)
|
||||
|
|
@ -313,7 +279,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
|
||||
context 'for Issue comment removed actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_COMMENT_REMOVED }
|
||||
|
||||
def track_action(params)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Utils do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
delegate :to_boolean, :boolean_to_yes_no, :slugify, :random_string, :which,
|
||||
:ensure_array_from_string, :to_exclusive_sentence, :bytes_to_megabytes,
|
||||
:append_path, :check_path_traversal!, :allowlisted?, :check_allowed_absolute_path!, :decode_path, :ms_to_round_sec, to: :described_class
|
||||
|
|
@ -50,6 +52,10 @@ RSpec.describe Gitlab::Utils do
|
|||
expect(check_path_traversal!('dir/..foo.rb')).to eq('dir/..foo.rb')
|
||||
expect(check_path_traversal!('dir/.foo.rb')).to eq('dir/.foo.rb')
|
||||
end
|
||||
|
||||
it 'does nothing for a non-string' do
|
||||
expect(check_path_traversal!(nil)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '.allowlisted?' do
|
||||
|
|
@ -448,4 +454,29 @@ RSpec.describe Gitlab::Utils do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.valid_brackets?' do
|
||||
where(:input, :allow_nested, :valid) do
|
||||
'no brackets' | true | true
|
||||
'no brackets' | false | true
|
||||
'user[avatar]' | true | true
|
||||
'user[avatar]' | false | true
|
||||
'user[avatar][friends]' | true | true
|
||||
'user[avatar][friends]' | false | true
|
||||
'user[avatar[image[url]]]' | true | true
|
||||
'user[avatar[image[url]]]' | false | false
|
||||
'user[avatar[]friends]' | true | true
|
||||
'user[avatar[]friends]' | false | false
|
||||
'user[avatar]]' | true | false
|
||||
'user[avatar]]' | false | false
|
||||
'user][avatar]]' | true | false
|
||||
'user][avatar]]' | false | false
|
||||
'user[avatar' | true | false
|
||||
'user[avatar' | false | false
|
||||
end
|
||||
|
||||
with_them do
|
||||
it { expect(described_class.valid_brackets?(input, allow_nested: allow_nested)).to eq(valid) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -122,6 +122,21 @@ RSpec.describe Packages::Package, type: :model do
|
|||
it { is_expected.not_to allow_value('my file name').for(:name) }
|
||||
it { is_expected.not_to allow_value('!!().for(:name)().for(:name)').for(:name) }
|
||||
end
|
||||
|
||||
context 'nuget package' do
|
||||
subject { build_stubbed(:nuget_package) }
|
||||
|
||||
it { is_expected.to allow_value('My.Package').for(:name) }
|
||||
it { is_expected.to allow_value('My.Package.Mvc').for(:name) }
|
||||
it { is_expected.to allow_value('MyPackage').for(:name) }
|
||||
it { is_expected.to allow_value('My.23.Package').for(:name) }
|
||||
it { is_expected.to allow_value('My23Package').for(:name) }
|
||||
it { is_expected.to allow_value('runtime.my-test64.runtime.package.Mvc').for(:name) }
|
||||
it { is_expected.to allow_value('my_package').for(:name) }
|
||||
it { is_expected.not_to allow_value('My/package').for(:name) }
|
||||
it { is_expected.not_to allow_value('../../../my_package').for(:name) }
|
||||
it { is_expected.not_to allow_value('%2e%2e%2fmy_package').for(:name) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#version' do
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ RSpec.describe ProjectPolicy do
|
|||
it 'disallows all permissions except pipeline when the feature is disabled' do
|
||||
builds_permissions = [
|
||||
:create_build, :read_build, :update_build, :admin_build, :destroy_build,
|
||||
:create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
|
||||
:create_pipeline_schedule, :read_pipeline_schedule_variables, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
|
||||
:create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
|
||||
:create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster,
|
||||
:create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment
|
||||
|
|
|
|||
|
|
@ -97,14 +97,50 @@ RSpec.describe API::Ci::PipelineSchedules do
|
|||
pipeline_schedule.pipelines << build(:ci_pipeline, project: project)
|
||||
end
|
||||
|
||||
context 'authenticated user with valid permissions' do
|
||||
it 'returns pipeline_schedule details' do
|
||||
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer)
|
||||
|
||||
matcher :return_pipeline_schedule_sucessfully do
|
||||
match_unless_raises do |reponse|
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to match_response_schema('pipeline_schedule')
|
||||
end
|
||||
end
|
||||
|
||||
shared_context 'request with project permissions' do
|
||||
context 'authenticated user with project permisions' do
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
it 'returns pipeline_schedule details' do
|
||||
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user)
|
||||
|
||||
expect(response).to return_pipeline_schedule_sucessfully
|
||||
expect(json_response).to have_key('variables')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'request with schedule ownership' do
|
||||
context 'authenticated user with pipeline schedule ownership' do
|
||||
it 'returns pipeline_schedule details' do
|
||||
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer)
|
||||
|
||||
expect(response).to return_pipeline_schedule_sucessfully
|
||||
expect(json_response).to have_key('variables')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'request with unauthenticated user' do
|
||||
context 'with unauthenticated user' do
|
||||
it 'does not return pipeline_schedule' do
|
||||
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}")
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'request with non-existing pipeline_schedule' do
|
||||
it 'responds with 404 Not Found if requesting non-existing pipeline_schedule' do
|
||||
get api("/projects/#{project.id}/pipeline_schedules/-5", developer)
|
||||
|
||||
|
|
@ -112,31 +148,61 @@ RSpec.describe API::Ci::PipelineSchedules do
|
|||
end
|
||||
end
|
||||
|
||||
context 'authenticated user with invalid permissions' do
|
||||
it 'does not return pipeline_schedules list' do
|
||||
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user)
|
||||
context 'with private project' do
|
||||
it_behaves_like 'request with schedule ownership'
|
||||
it_behaves_like 'request with project permissions'
|
||||
it_behaves_like 'request with unauthenticated user'
|
||||
it_behaves_like 'request with non-existing pipeline_schedule'
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
context 'authenticated user with no project permissions' do
|
||||
it 'does not return pipeline_schedule' do
|
||||
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'authenticated user with insufficient project permissions' do
|
||||
before do
|
||||
project.add_guest(user)
|
||||
end
|
||||
|
||||
it 'does not return pipeline_schedule' do
|
||||
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'authenticated user with insufficient permissions' do
|
||||
before do
|
||||
project.add_guest(user)
|
||||
context 'with public project' do
|
||||
let_it_be(:project) { create(:project, :repository, :public, public_builds: false) }
|
||||
|
||||
it_behaves_like 'request with schedule ownership'
|
||||
it_behaves_like 'request with project permissions'
|
||||
it_behaves_like 'request with unauthenticated user'
|
||||
it_behaves_like 'request with non-existing pipeline_schedule'
|
||||
|
||||
context 'authenticated user with no project permissions' do
|
||||
it 'returns pipeline_schedule with no variables' do
|
||||
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user)
|
||||
|
||||
expect(response).to return_pipeline_schedule_sucessfully
|
||||
expect(json_response).not_to have_key('variables')
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not return pipeline_schedules list' do
|
||||
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user)
|
||||
context 'authenticated user with insufficient project permissions' do
|
||||
before do
|
||||
project.add_guest(user)
|
||||
end
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
it 'returns pipeline_schedule with no variables' do
|
||||
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user)
|
||||
|
||||
context 'unauthenticated user' do
|
||||
it 'does not return pipeline_schedules list' do
|
||||
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}")
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response).to return_pipeline_schedule_sucessfully
|
||||
expect(json_response).not_to have_key('variables')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -166,6 +166,16 @@ RSpec.describe API::Internal::Kubernetes do
|
|||
)
|
||||
)
|
||||
end
|
||||
|
||||
context 'repository is for project members only' do
|
||||
let(:project) { create(:project, :public, :repository_private) }
|
||||
|
||||
it 'returns 404' do
|
||||
send_request(params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'project is private' do
|
||||
|
|
@ -190,7 +200,7 @@ RSpec.describe API::Internal::Kubernetes do
|
|||
|
||||
context 'project does not exist' do
|
||||
it 'returns 404' do
|
||||
send_request(params: { id: 0 }, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
|
||||
send_request(params: { id: non_existing_record_id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache do
|
|||
let(:export_path) { "#{Dir.tmpdir}/project_export_spec" }
|
||||
|
||||
before do
|
||||
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
|
||||
allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
|
||||
allow_next_instance_of(ProjectExportWorker) do |job|
|
||||
allow(job).to receive(:jid).and_return(SecureRandom.hex(8))
|
||||
end
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ RSpec.describe API::Terraform::State do
|
|||
expect { request }.to change { Terraform::State.count }.by(0)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(Gitlab::Json.parse(response.body)).to be_empty
|
||||
end
|
||||
|
||||
context 'on Unicorn', :unicorn do
|
||||
|
|
@ -132,6 +133,7 @@ RSpec.describe API::Terraform::State do
|
|||
expect { request }.to change { Terraform::State.count }.by(0)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(Gitlab::Json.parse(response.body)).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -167,6 +169,7 @@ RSpec.describe API::Terraform::State do
|
|||
expect { request }.to change { Terraform::State.count }.by(1)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(Gitlab::Json.parse(response.body)).to be_empty
|
||||
end
|
||||
|
||||
context 'on Unicorn', :unicorn do
|
||||
|
|
@ -174,6 +177,7 @@ RSpec.describe API::Terraform::State do
|
|||
expect { request }.to change { Terraform::State.count }.by(1)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(Gitlab::Json.parse(response.body)).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -218,10 +222,11 @@ RSpec.describe API::Terraform::State do
|
|||
context 'with maintainer permissions' do
|
||||
let(:current_user) { maintainer }
|
||||
|
||||
it 'deletes the state' do
|
||||
it 'deletes the state and returns empty body' do
|
||||
expect { request }.to change { Terraform::State.count }.by(-1)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(Gitlab::Json.parse(response.body)).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -198,24 +198,26 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
|
|||
it_behaves_like 'raising an', ::Packages::Nuget::MetadataExtractionService::ExtractionError
|
||||
end
|
||||
|
||||
context 'with package file with a blank package name' do
|
||||
before do
|
||||
allow(service).to receive(:package_name).and_return('')
|
||||
context 'with an invalid package name' do
|
||||
invalid_names = [
|
||||
'',
|
||||
'My/package',
|
||||
'../../../my_package',
|
||||
'%2e%2e%2fmy_package'
|
||||
]
|
||||
|
||||
invalid_names.each do |invalid_name|
|
||||
before do
|
||||
allow(service).to receive(:package_name).and_return(invalid_name)
|
||||
end
|
||||
|
||||
it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError
|
||||
end
|
||||
|
||||
it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError
|
||||
end
|
||||
|
||||
context 'with package file with a blank package version' do
|
||||
before do
|
||||
allow(service).to receive(:package_version).and_return('')
|
||||
end
|
||||
|
||||
it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError
|
||||
end
|
||||
|
||||
context 'with an invalid package version' do
|
||||
invalid_versions = [
|
||||
'',
|
||||
'555',
|
||||
'1.2',
|
||||
'1./2.3',
|
||||
|
|
@ -224,13 +226,11 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
|
|||
]
|
||||
|
||||
invalid_versions.each do |invalid_version|
|
||||
it "raises an error for version #{invalid_version}" do
|
||||
before do
|
||||
allow(service).to receive(:package_version).and_return(invalid_version)
|
||||
|
||||
expect { subject }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Version is invalid')
|
||||
expect(package_file.file_name).not_to include(invalid_version)
|
||||
expect(package_file.file.file.path).not_to include(invalid_version)
|
||||
end
|
||||
|
||||
it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,16 +11,15 @@ RSpec.describe ResourceAccessTokens::CreateService do
|
|||
|
||||
describe '#execute' do
|
||||
# Created shared_examples as it will easy to include specs for group bots in https://gitlab.com/gitlab-org/gitlab/-/issues/214046
|
||||
shared_examples 'fails when user does not have the permission to create a Resource Bot' do
|
||||
before_all do
|
||||
resource.add_developer(user)
|
||||
shared_examples 'token creation fails' do
|
||||
let(:resource) { create(:project)}
|
||||
|
||||
it 'does not add the project bot as a member' do
|
||||
expect { subject }.not_to change { resource.members.count }
|
||||
end
|
||||
|
||||
it 'returns error' do
|
||||
response = subject
|
||||
|
||||
expect(response.error?).to be true
|
||||
expect(response.message).to eq("User does not have permission to create #{resource_type} Access Token")
|
||||
it 'immediately destroys the bot user if one was created', :sidekiq_inline do
|
||||
expect { subject }.not_to change { User.bots.count }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -154,24 +153,36 @@ RSpec.describe ResourceAccessTokens::CreateService do
|
|||
context 'when invalid scope is passed' do
|
||||
let_it_be(:params) { { scopes: [:invalid_scope] } }
|
||||
|
||||
it 'returns error' do
|
||||
it_behaves_like 'token creation fails'
|
||||
|
||||
it 'returns the scope error message' do
|
||||
response = subject
|
||||
|
||||
expect(response.error?).to be true
|
||||
expect(response.errors).to include("Scopes can only contain available scopes")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when access provisioning fails' do
|
||||
before do
|
||||
allow(resource).to receive(:add_user).and_return(nil)
|
||||
end
|
||||
context "when access provisioning fails" do
|
||||
let_it_be(:bot_user) { create(:user, :project_bot) }
|
||||
let(:unpersisted_member) { build(:project_member, source: resource, user: bot_user) }
|
||||
|
||||
it 'returns error' do
|
||||
response = subject
|
||||
before do
|
||||
allow_next_instance_of(ResourceAccessTokens::CreateService) do |service|
|
||||
allow(service).to receive(:create_user).and_return(bot_user)
|
||||
allow(service).to receive(:create_membership).and_return(unpersisted_member)
|
||||
end
|
||||
end
|
||||
|
||||
expect(response.error?).to be true
|
||||
it_behaves_like 'token creation fails'
|
||||
|
||||
it 'returns the provisioning error message' do
|
||||
response = subject
|
||||
|
||||
expect(response.error?).to be true
|
||||
expect(response.errors).to include("Could not provision maintainer access to project access token")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -180,7 +191,16 @@ RSpec.describe ResourceAccessTokens::CreateService do
|
|||
let_it_be(:resource_type) { 'project' }
|
||||
let_it_be(:resource) { project }
|
||||
|
||||
it_behaves_like 'fails when user does not have the permission to create a Resource Bot'
|
||||
context 'when user does not have permission to create a resource bot' do
|
||||
it_behaves_like 'token creation fails'
|
||||
|
||||
it 'returns the permission error message' do
|
||||
response = subject
|
||||
|
||||
expect(response.error?).to be true
|
||||
expect(response.errors).to include("User does not have permission to create #{resource_type} Access Token")
|
||||
end
|
||||
end
|
||||
|
||||
context 'user with valid permission' do
|
||||
before_all do
|
||||
|
|
|
|||
|
|
@ -42,17 +42,17 @@ RSpec.describe Terraform::RemoteStateHandler do
|
|||
|
||||
describe '#handle_with_lock' do
|
||||
it 'allows to modify a state using database locking' do
|
||||
state = subject.handle_with_lock do |state|
|
||||
record = nil
|
||||
subject.handle_with_lock do |state|
|
||||
record = state
|
||||
state.name = 'updated-name'
|
||||
end
|
||||
|
||||
expect(state.name).to eq 'updated-name'
|
||||
expect(record.reload.name).to eq 'updated-name'
|
||||
end
|
||||
|
||||
it 'returns the state object itself' do
|
||||
state = subject.handle_with_lock
|
||||
|
||||
expect(state.name).to eq 'my-state'
|
||||
it 'returns nil' do
|
||||
expect(subject.handle_with_lock).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -70,11 +70,13 @@ RSpec.describe Terraform::RemoteStateHandler do
|
|||
it 'handles a locked state using exclusive read lock' do
|
||||
handler.lock!
|
||||
|
||||
state = handler.handle_with_lock do |state|
|
||||
record = nil
|
||||
handler.handle_with_lock do |state|
|
||||
record = state
|
||||
state.name = 'new-name'
|
||||
end
|
||||
|
||||
expect(state.name).to eq 'new-name'
|
||||
expect(record.reload.name).to eq 'new-name'
|
||||
end
|
||||
|
||||
it 'raises exception if lock has not been acquired before' do
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ module ImportExport
|
|||
export_path = [prefix, 'spec', 'fixtures', 'lib', 'gitlab', 'import_export', name].compact
|
||||
export_path = File.join(*export_path)
|
||||
|
||||
allow_any_instance_of(Gitlab::ImportExport).to receive(:export_path) { export_path }
|
||||
allow(Gitlab::ImportExport).to receive(:export_path) { export_path }
|
||||
end
|
||||
|
||||
def setup_reader(reader)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This patch allows stubbing of prepended methods
|
||||
# Based on https://github.com/rspec/rspec-mocks/pull/1218
|
||||
|
||||
module RSpec
|
||||
module Mocks
|
||||
module InstanceMethodStasherForPrependedMethods
|
||||
private
|
||||
|
||||
def method_owned_by_klass?
|
||||
owner = @klass.instance_method(@method).owner
|
||||
owner = owner.class unless Module === owner
|
||||
|
||||
owner == @klass ||
|
||||
# When `extend self` is used, and not under any instance of
|
||||
(owner.singleton_class == @klass && !Mocks.space.any_instance_recorder_for(owner, true)) ||
|
||||
!method_defined_on_klass?(owner)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module RSpec
|
||||
module Mocks
|
||||
module MethodDoubleForPrependedMethods
|
||||
def restore_original_method
|
||||
return show_frozen_warning if object_singleton_class.frozen?
|
||||
return unless @method_is_proxied
|
||||
|
||||
remove_method_from_definition_target
|
||||
|
||||
if @method_stasher.method_is_stashed?
|
||||
@method_stasher.restore
|
||||
restore_original_visibility
|
||||
end
|
||||
|
||||
@method_is_proxied = false
|
||||
end
|
||||
|
||||
def restore_original_visibility
|
||||
method_owner.__send__(@original_visibility, @method_name)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def method_owner
|
||||
@method_owner ||= Object.instance_method(:method).bind(object).call(@method_name).owner
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec::Mocks::InstanceMethodStasher.prepend(RSpec::Mocks::InstanceMethodStasherForPrependedMethods)
|
||||
RSpec::Mocks::MethodDouble.prepend(RSpec::Mocks::MethodDoubleForPrependedMethods)
|
||||
|
|
@ -73,7 +73,23 @@ RSpec.shared_examples 'project access tokens available #create' do
|
|||
end
|
||||
end
|
||||
|
||||
it { expect(subject).to render_template(:index) }
|
||||
it 'does not create the token' do
|
||||
expect { subject }.not_to change { PersonalAccessToken.count }
|
||||
end
|
||||
|
||||
it 'does not add the project bot as a member' do
|
||||
expect { subject }.not_to change { Member.count }
|
||||
end
|
||||
|
||||
it 'does not create the project bot user' do
|
||||
expect { subject }.not_to change { User.count }
|
||||
end
|
||||
|
||||
it 'shows a failure alert' do
|
||||
subject
|
||||
|
||||
expect(response.flash[:alert]).to match("Failed to create new project access token: Failed!")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'a tracked issue edit event' do |event|
|
||||
before do
|
||||
stub_application_setting(usage_ping_enabled: true)
|
||||
end
|
||||
|
||||
def count_unique(date_from:, date_to:)
|
||||
Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: action, start_date: date_from, end_date: date_to)
|
||||
end
|
||||
|
||||
specify do
|
||||
aggregate_failures do
|
||||
expect(track_action(author: user1)).to be_truthy
|
||||
expect(track_action(author: user1)).to be_truthy
|
||||
expect(track_action(author: user2)).to be_truthy
|
||||
expect(track_action(author: user3, time: time - 3.days)).to be_truthy
|
||||
|
||||
expect(count_unique(date_from: time, date_to: time)).to eq(2)
|
||||
expect(count_unique(date_from: time - 5.days, date_to: 1.day.since(time))).to eq(3)
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not track edit actions if author is not present' do
|
||||
expect(track_action(author: nil)).to be_nil
|
||||
end
|
||||
|
||||
context 'when feature flag track_issue_activity_actions is disabled' do
|
||||
it 'does not track edit actions' do
|
||||
stub_feature_flags(track_issue_activity_actions: false)
|
||||
|
||||
expect(track_action(author: user1)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -14,6 +14,7 @@ end
|
|||
|
||||
RSpec.shared_examples "builds correct paths" do |**patterns|
|
||||
let(:patterns) { patterns }
|
||||
let(:fixture) { File.join('spec', 'fixtures', 'rails_sample.jpg') }
|
||||
|
||||
before do
|
||||
allow(subject).to receive(:filename).and_return('<filename>')
|
||||
|
|
@ -55,4 +56,15 @@ RSpec.shared_examples "builds correct paths" do |**patterns|
|
|||
let(:target) { subject.class }
|
||||
end
|
||||
end
|
||||
|
||||
describe "path traversal exploits" do
|
||||
before do
|
||||
allow(subject).to receive(:filename).and_return("3bc58d54542d6a5efffa9a87554faac0254f73f675b337899ea869f6d38b7371/122../../../../../../../../.ssh/authorized_keys")
|
||||
end
|
||||
|
||||
it "throws an exception" do
|
||||
expect { subject.cache!(fixture_file_upload(fixture)) }.to raise_error(Gitlab::Utils::PathTraversalAttackError)
|
||||
expect { subject.store!(fixture_file_upload(fixture)) }.to raise_error(Gitlab::Utils::PathTraversalAttackError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -24,9 +24,14 @@ RSpec.describe ImportExportUploader do
|
|||
|
||||
include_context 'with storage', described_class::Store::REMOTE
|
||||
|
||||
it_behaves_like 'builds correct paths',
|
||||
store_dir: %r[import_export_upload/import_file/],
|
||||
upload_path: %r[import_export_upload/import_file/]
|
||||
patterns = {
|
||||
store_dir: %r[import_export_upload/import_file/],
|
||||
upload_path: %r[import_export_upload/import_file/]
|
||||
}
|
||||
|
||||
it_behaves_like 'builds correct paths', patterns do
|
||||
let(:fixture) { File.join('spec', 'fixtures', 'group_export.tar.gz') }
|
||||
end
|
||||
|
||||
describe '#move_to_store' do
|
||||
it 'returns false' do
|
||||
|
|
|
|||
|
|
@ -13,6 +13,18 @@ RSpec.describe Packages::Nuget::ExtractionWorker, type: :worker do
|
|||
|
||||
subject { described_class.new.perform(package_file_id) }
|
||||
|
||||
shared_examples 'handling the metadata error' do |exception_class: ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError|
|
||||
it 'removes the package and the package file' do
|
||||
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
|
||||
instance_of(exception_class),
|
||||
project_id: package.project_id
|
||||
)
|
||||
expect { subject }
|
||||
.to change { Packages::Package.count }.by(-1)
|
||||
.and change { Packages::PackageFile.count }.by(-1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid package file' do
|
||||
it 'updates package and package file' do
|
||||
expect { subject }
|
||||
|
|
@ -48,46 +60,46 @@ RSpec.describe Packages::Nuget::ExtractionWorker, type: :worker do
|
|||
allow_any_instance_of(Zip::File).to receive(:glob).and_return([])
|
||||
end
|
||||
|
||||
it 'removes the package and the package file' do
|
||||
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
|
||||
instance_of(::Packages::Nuget::MetadataExtractionService::ExtractionError),
|
||||
project_id: package.project_id
|
||||
)
|
||||
expect { subject }
|
||||
.to change { Packages::Package.count }.by(-1)
|
||||
.and change { Packages::PackageFile.count }.by(-1)
|
||||
it_behaves_like 'handling the metadata error', exception_class: ::Packages::Nuget::MetadataExtractionService::ExtractionError
|
||||
end
|
||||
|
||||
context 'with package with an invalid package name' do
|
||||
invalid_names = [
|
||||
'',
|
||||
'My/package',
|
||||
'../../../my_package',
|
||||
'%2e%2e%2fmy_package'
|
||||
]
|
||||
|
||||
invalid_names.each do |invalid_name|
|
||||
before do
|
||||
allow_next_instance_of(::Packages::Nuget::UpdatePackageFromMetadataService) do |service|
|
||||
allow(service).to receive(:package_name).and_return(invalid_name)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'handling the metadata error'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with package file with a blank package name' do
|
||||
before do
|
||||
allow_any_instance_of(::Packages::Nuget::UpdatePackageFromMetadataService).to receive(:package_name).and_return('')
|
||||
end
|
||||
context 'with package with an invalid package version' do
|
||||
invalid_versions = [
|
||||
'',
|
||||
'555',
|
||||
'1.2',
|
||||
'1./2.3',
|
||||
'../../../../../1.2.3',
|
||||
'%2e%2e%2f1.2.3'
|
||||
]
|
||||
|
||||
it 'removes the package and the package file' do
|
||||
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
|
||||
instance_of(::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError),
|
||||
project_id: package.project_id
|
||||
)
|
||||
expect { subject }
|
||||
.to change { Packages::Package.count }.by(-1)
|
||||
.and change { Packages::PackageFile.count }.by(-1)
|
||||
end
|
||||
end
|
||||
invalid_versions.each do |invalid_version|
|
||||
before do
|
||||
allow_next_instance_of(::Packages::Nuget::UpdatePackageFromMetadataService) do |service|
|
||||
allow(service).to receive(:package_version).and_return(invalid_version)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with package file with a blank package version' do
|
||||
before do
|
||||
allow_any_instance_of(::Packages::Nuget::UpdatePackageFromMetadataService).to receive(:package_version).and_return('')
|
||||
end
|
||||
|
||||
it 'removes the package and the package file' do
|
||||
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
|
||||
instance_of(::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError),
|
||||
project_id: package.project_id
|
||||
)
|
||||
expect { subject }
|
||||
.to change { Packages::Package.count }.by(-1)
|
||||
.and change { Packages::PackageFile.count }.by(-1)
|
||||
it_behaves_like 'handling the metadata error'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue