Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-04-23 09:10:03 +00:00
parent 1165608bfd
commit 65f7976d0c
72 changed files with 1305 additions and 310 deletions

View File

@ -1 +1 @@
ffbce774bce90b5a65f5b235afe492a7266aa82f
16479e5771b69a2b4c22aade7c0a7fc2a6f897ce

View File

@ -20,6 +20,7 @@ import axios from '~/lib/utils/axios_utils';
import csrf from '~/lib/utils/csrf';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import validation from '~/vue_shared/directives/validation';
const PRIVATE_VISIBILITY = 'private';
const INTERNAL_VISIBILITY = 'internal';
@ -31,6 +32,13 @@ const ALLOWED_VISIBILITY = {
public: [INTERNAL_VISIBILITY, PRIVATE_VISIBILITY, PUBLIC_VISIBILITY],
};
const initFormField = ({ value, required = true, skipValidation = false }) => ({
value,
required,
state: skipValidation ? true : null,
feedback: null,
});
export default {
components: {
GlForm,
@ -46,6 +54,9 @@ export default {
GlFormRadioGroup,
GlFormSelect,
},
directives: {
validation: validation(),
},
inject: {
newGroupPath: {
default: '',
@ -77,7 +88,8 @@ export default {
},
projectDescription: {
type: String,
required: true,
required: false,
default: '',
},
projectVisibility: {
type: String,
@ -85,16 +97,30 @@ export default {
},
},
data() {
const form = {
state: false,
showValidation: false,
fields: {
namespace: initFormField({
value: null,
}),
name: initFormField({ value: this.projectName }),
slug: initFormField({ value: this.projectPath }),
description: initFormField({
value: this.projectDescription,
required: false,
skipValidation: true,
}),
visibility: initFormField({
value: this.projectVisibility,
skipValidation: true,
}),
},
};
return {
isSaving: false,
namespaces: [],
selectedNamespace: {},
fork: {
name: this.projectName,
slug: this.projectPath,
description: this.projectDescription,
visibility: this.projectVisibility,
},
form,
};
},
computed: {
@ -106,7 +132,7 @@ export default {
},
namespaceAllowedVisibility() {
return (
ALLOWED_VISIBILITY[this.selectedNamespace.visibility] ||
ALLOWED_VISIBILITY[this.form.fields.namespace.value?.visibility] ||
ALLOWED_VISIBILITY[PUBLIC_VISIBILITY]
);
},
@ -139,16 +165,17 @@ export default {
},
},
watch: {
selectedNamespace(newVal) {
// eslint-disable-next-line func-names
'form.fields.namespace.value': function (newVal) {
const { visibility } = newVal;
if (this.projectAllowedVisibility.includes(visibility)) {
this.fork.visibility = visibility;
this.form.fields.visibility.value = visibility;
}
},
// eslint-disable-next-line func-names
'fork.name': function (newVal) {
this.fork.slug = kebabCase(newVal);
'form.fields.name.value': function (newVal) {
this.form.fields.slug.value = kebabCase(newVal);
},
},
mounted() {
@ -166,19 +193,25 @@ export default {
);
},
async onSubmit() {
this.form.showValidation = true;
if (!this.form.state) {
return;
}
this.isSaving = true;
this.form.showValidation = false;
const { projectId } = this;
const { name, slug, description, visibility } = this.fork;
const { id: namespaceId } = this.selectedNamespace;
const { name, slug, description, visibility, namespace } = this.form.fields;
const postParams = {
id: projectId,
name,
namespace_id: namespaceId,
path: slug,
description,
visibility,
name: name.value,
namespace_id: namespace.value.id,
path: slug.value,
description: description.value,
visibility: visibility.value,
};
const forkProjectPath = `/api/:version/projects/:id/fork`;
@ -198,16 +231,34 @@ export default {
</script>
<template>
<gl-form method="POST" @submit.prevent="onSubmit">
<gl-form novalidate method="POST" @submit.prevent="onSubmit">
<input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
<gl-form-group label="Project name" label-for="fork-name">
<gl-form-input id="fork-name" v-model="fork.name" data-testid="fork-name-input" required />
<gl-form-group
:label="__('Project name')"
label-for="fork-name"
:invalid-feedback="form.fields.name.feedback"
>
<gl-form-input
id="fork-name"
v-model="form.fields.name.value"
v-validation:[form.showValidation]
name="name"
data-testid="fork-name-input"
:state="form.fields.name.state"
required
/>
</gl-form-group>
<div class="gl-md-display-flex">
<div class="gl-flex-basis-half">
<gl-form-group label="Project URL" label-for="fork-url" class="gl-md-mr-3">
<gl-form-group
:label="__('Project URL')"
label-for="fork-url"
class="gl-md-mr-3"
:state="form.fields.namespace.state"
:invalid-feedback="s__('ForkProject|Please select a namespace')"
>
<gl-form-input-group>
<template #prepend>
<gl-input-group-text>
@ -216,9 +267,12 @@ export default {
</template>
<gl-form-select
id="fork-url"
v-model="selectedNamespace"
v-model="form.fields.namespace.value"
v-validation:[form.showValidation]
name="namespace"
data-testid="fork-url-input"
data-qa-selector="fork_namespace_dropdown"
:state="form.fields.namespace.state"
required
>
<template slot="first">
@ -232,11 +286,19 @@ export default {
</gl-form-group>
</div>
<div class="gl-flex-basis-half">
<gl-form-group label="Project slug" label-for="fork-slug" class="gl-md-ml-3">
<gl-form-group
:label="__('Project slug')"
label-for="fork-slug"
class="gl-md-ml-3"
:invalid-feedback="form.fields.slug.feedback"
>
<gl-form-input
id="fork-slug"
v-model="fork.slug"
v-model="form.fields.slug.value"
v-validation:[form.showValidation]
data-testid="fork-slug-input"
name="slug"
:state="form.fields.slug.state"
required
/>
</gl-form-group>
@ -250,11 +312,13 @@ export default {
</gl-link>
</p>
<gl-form-group label="Project description (optional)" label-for="fork-description">
<gl-form-group :label="__('Project description (optional)')" label-for="fork-description">
<gl-form-textarea
id="fork-description"
v-model="fork.description"
v-model="form.fields.description.value"
data-testid="fork-description-textarea"
name="description"
:state="form.fields.description.state"
/>
</gl-form-group>
@ -266,8 +330,9 @@ export default {
</gl-link>
</label>
<gl-form-radio-group
v-model="fork.visibility"
v-model="form.fields.visibility.value"
data-testid="fork-visibility-radio-group"
name="visibility"
required
>
<gl-form-radio
@ -291,6 +356,7 @@ export default {
type="submit"
category="primary"
variant="confirm"
class="js-no-auto-disable"
data-testid="submit-button"
data-qa-selector="fork_project_button"
:loading="isSaving"

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
# This is based on https://github.com/rmosolgo/graphql-ruby/blob/v1.11.8/lib/graphql/subscriptions/action_cable_subscriptions.rb#L19-L82
# modified to work with our own ActionCableLink client
class GraphqlChannel < ApplicationCable::Channel # rubocop:disable Gitlab/NamespacedClass
def subscribed
@subscription_ids = []
query = params['query']
variables = Gitlab::Graphql::Variables.new(params['variables']).to_h
operation_name = params['operationName']
result = GitlabSchema.execute(
query,
context: context,
variables: variables,
operation_name: operation_name
)
payload = {
result: result.to_h,
more: result.subscription?
}
# Track the subscription here so we can remove it
# on unsubscribe.
if result.context[:subscription_id]
@subscription_ids << result.context[:subscription_id]
end
transmit(payload)
end
def unsubscribed
@subscription_ids.each do |sid|
GitlabSchema.subscriptions.delete_subscription(sid)
end
end
rescue_from Gitlab::Graphql::Variables::Invalid do |exception|
transmit({ errors: [{ message: exception.message }] })
end
private
# When modifying the context, also update GraphqlController#context if needed
# so that we have similar context when executing queries, mutations, and subscriptions
#
# Objects added to the context may also need to be reloaded in
# `Subscriptions::BaseSubscription` so that they are not stale
def context
# is_sessionless_user is always false because we only support cookie auth in ActionCable
{ channel: self, current_user: current_user, is_sessionless_user: false }
end
end

View File

@ -109,6 +109,8 @@ class GraphqlController < ApplicationController
end
end
# When modifying the context, also update GraphqlChannel#context if needed
# so that we have similar context when executing queries, mutations, and subscriptions
def context
@context ||= { current_user: current_user, is_sessionless_user: !!sessionless_user?, request: request }
end

View File

@ -10,6 +10,7 @@ class GitlabSchema < GraphQL::Schema
DEFAULT_MAX_DEPTH = 15
AUTHENTICATED_MAX_DEPTH = 20
use GraphQL::Subscriptions::ActionCableSubscriptions
use GraphQL::Pagination::Connections
use BatchLoader::GraphQL
use Gitlab::Graphql::Pagination::Connections
@ -24,6 +25,7 @@ class GitlabSchema < GraphQL::Schema
query Types::QueryType
mutation Types::MutationType
subscription Types::SubscriptionType
default_max_page_size 100

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
module GraphqlTriggers
def self.issuable_assignees_updated(issuable)
GitlabSchema.subscriptions.trigger('issuableAssigneesUpdated', { issuable_id: issuable.to_gid }, issuable)
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
module Subscriptions
class BaseSubscription < GraphQL::Schema::Subscription
object_class Types::BaseObject
field_class Types::BaseField
def initialize(object:, context:, field:)
super
# Reset user so that we don't use a stale user for authorization
current_user.reset if current_user
end
def authorized?(*)
raise NotImplementedError
end
private
def unauthorized!
unsubscribe if context.query.subscription_update?
raise GraphQL::ExecutionError, 'Unauthorized subscription'
end
def current_user
context[:current_user]
end
end
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
module Subscriptions
class IssuableUpdated < BaseSubscription
include Gitlab::Graphql::Laziness
payload_type Types::IssuableType
argument :issuable_id, Types::GlobalIDType[Issuable],
required: true,
description: 'ID of the issuable.'
def subscribe(issuable_id:)
nil
end
def authorized?(issuable_id:)
# TODO: remove this check when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
raise Gitlab::Graphql::Errors::ArgumentError, 'Invalid IssuableID' unless issuable_id.is_a?(GlobalID)
issuable = force(GitlabSchema.find_by_gid(issuable_id))
unauthorized! unless issuable && Ability.allowed?(current_user, :"read_#{issuable.to_ability_name}", issuable)
true
end
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
module Types
class IssuableType < BaseUnion
graphql_name 'Issuable'
description 'Represents an issuable.'
possible_types Types::IssueType, Types::MergeRequestType
def self.resolve_type(object, context)
case object
when Issue
Types::IssueType
when MergeRequest
Types::MergeRequestType
else
raise 'Unsupported issuable type'
end
end
end
end
Types::IssuableType.prepend_if_ee('::EE::Types::IssuableType')

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
module Types
class SubscriptionType < ::Types::BaseObject
graphql_name 'Subscription'
field :issuable_assignees_updated, subscription: Subscriptions::IssuableUpdated, null: true,
description: 'Triggered when the assignees of an issuable are updated.'
end
end

View File

@ -393,7 +393,7 @@ module ApplicationSettingsHelper
end
def integration_expanded?(substring)
@application_setting.errors.any? { |k| k.to_s.start_with?(substring) }
@application_setting.errors.messages.any? { |k, _| k.to_s.start_with?(substring) }
end
def instance_clusters_enabled?

View File

@ -124,7 +124,9 @@ module Routable
def set_path_errors
route_path_errors = self.errors.delete(:"route.path")
self.errors[:path].concat(route_path_errors) if route_path_errors
route_path_errors&.each do |msg|
self.errors.add(:path, msg)
end
end
def full_name_changed?

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class YoutrackService < IssueTrackerService
include ActionView::Helpers::UrlHelper
validates :project_url, :issues_url, presence: true, public_url: true, if: :activated?
# {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1, gl-030
@ -17,7 +19,12 @@ class YoutrackService < IssueTrackerService
end
def description
s_('IssueTracker|YouTrack issue tracker')
s_("IssueTracker|Use YouTrack as this project's issue tracker.")
end
def help
docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/youtrack'), target: '_blank', rel: 'noopener noreferrer'
s_("IssueTracker|Use YouTrack as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param

View File

@ -1414,7 +1414,9 @@ class User < ApplicationRecord
if namespace_path_errors.include?('has already been taken') && !User.exists?(username: username)
self.errors.add(:base, :username_exists_as_a_different_namespace)
else
self.errors[:username].concat(namespace_path_errors)
namespace_path_errors.each do |msg|
self.errors.add(:username, msg)
end
end
end

View File

@ -61,12 +61,7 @@ module Issues
todo_service.update_issue(issue, current_user, old_mentioned_users)
end
if issue.assignees != old_assignees
create_assignee_note(issue, old_assignees)
notification_service.async.reassigned_issue(issue, current_user, old_assignees)
todo_service.reassigned_assignable(issue, current_user, old_assignees)
track_incident_action(current_user, issue, :incident_assigned)
end
handle_assignee_changes(issue, old_assignees)
if issue.previous_changes.include?('confidential')
# don't enqueue immediately to prevent todos removal in case of a mistake
@ -90,6 +85,19 @@ module Issues
end
end
def handle_assignee_changes(issue, old_assignees)
return if issue.assignees == old_assignees
create_assignee_note(issue, old_assignees)
notification_service.async.reassigned_issue(issue, current_user, old_assignees)
todo_service.reassigned_assignable(issue, current_user, old_assignees)
track_incident_action(current_user, issue, :incident_assigned)
if Gitlab::ActionCable::Config.in_app? || Feature.enabled?(:broadcast_issue_updates, issue.project)
GraphqlTriggers.issuable_assignees_updated(issue)
end
end
def handle_task_changes(issuable)
todo_service.resolve_todos_for_target(issuable, current_user)
todo_service.update_issue(issuable, current_user)

View File

@ -0,0 +1,106 @@
# frozen_string_literal: true
module Packages
module Debian
class GenerateDistributionKeyService
include Gitlab::Utils::StrongMemoize
def initialize(current_user:, params: {})
@current_user = current_user
@params = params
end
def execute
raise ArgumentError, 'Please provide a user' unless current_user.is_a?(User)
generate_key
end
private
attr_reader :current_user, :params
def passphrase
strong_memoize(:passphrase) do
params[:passphrase] || ::User.random_password
end
end
def pinentry_script_content
escaped_passphrase = Shellwords.escape(passphrase)
<<~EOF
#!/bin/sh
echo OK Pleased to meet you
while read -r cmd; do
case "$cmd" in
GETPIN) echo D #{escaped_passphrase}; echo OK;;
*) echo OK;;
esac
done
EOF
end
def using_pinentry
Gitlab::Gpg.using_tmp_keychain do
home_dir = Gitlab::Gpg.current_home_dir
File.write("#{home_dir}/pinentry.sh", pinentry_script_content, mode: 'w', perm: 0755)
File.write("#{home_dir}/gpg-agent.conf", "pinentry-program #{home_dir}/pinentry.sh\n", mode: 'w')
GPGME::Ctx.new(armor: true, offline: true) do |ctx|
yield ctx
end
end
end
def generate_key_params
# https://www.gnupg.org/documentation/manuals/gnupg/Unattended-GPG-key-generation.html
'<GnupgKeyParms format="internal">' + "\n" +
{
'Key-Type': params[:key_type] || 'RSA',
'Key-Length': params[:key_length] || 4096,
'Key-Usage': params[:key_usage] || 'sign',
'Name-Real': params[:name_real] || 'GitLab Debian repository',
'Name-Email': params[:name_email] || Gitlab.config.gitlab.email_reply_to,
'Name-Comment': params[:name_comment] || 'GitLab Debian repository automatic signing key',
'Expire-Date': params[:expire_date] || 0,
'Passphrase': passphrase
}.map { |k, v| "#{k}: #{v}\n" }.join +
'</GnupgKeyParms>'
end
def generate_key
using_pinentry do |ctx|
# Generate key
ctx.generate_key generate_key_params
key = ctx.keys.first # rubocop:disable Gitlab/KeysFirstAndValuesFirst
fingerprint = key.fingerprint
# Export private key
data = GPGME::Data.new
ctx.export_keys fingerprint, data, GPGME::EXPORT_MODE_SECRET
data.seek 0
private_key = data.read
# Export public key
data = GPGME::Data.new
ctx.export_keys fingerprint, data
data.seek 0
public_key = data.read
{
private_key: private_key,
public_key: public_key,
passphrase: passphrase,
fingerprint: fingerprint
}
end
end
end
end
end

View File

@ -13,10 +13,10 @@ module SystemNotes
protected
def create_note(note_summary)
note = Note.create(note_summary.note.merge(system: true))
note.system_note_metadata = SystemNoteMetadata.new(note_summary.metadata) if note_summary.metadata?
note_params = note_summary.note.merge(system: true)
note_params[:system_note_metadata] = SystemNoteMetadata.new(note_summary.metadata) if note_summary.metadata?
note
Note.create(note_params)
end
def content_tag(*args)

View File

@ -1,9 +1,9 @@
%ul.content-list
%li
.avatar-cell.d-none.d-sm-block
= user_avatar(user: user, user_name: user.name, css_class: 'd-none d-sm-inline avatar s40')
.avatar-cell
= user_avatar(user: user, size: 40, user_name: user.name)
.user-info
= link_to user_path(user), class: 'd-none d-sm-inline' do
= link_to user_path(user) do
.item-title
= user.name
= user_status(user)

View File

@ -0,0 +1,5 @@
---
title: Reduce number of SQL queries when creating SystemNotes
merge_request: 59102
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Review and revise YouTrack integration UI text
merge_request: 59998
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: 'Users search: fix avatar size'
merge_request: 59883
author:
type: fixed

View File

@ -0,0 +1,8 @@
---
name: optimize_linkable_attributes
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59983
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/328696
milestone: '13.12'
type: development
group: group::source code
default_enabled: false

View File

@ -129,6 +129,7 @@ The following metrics are available:
| `pipeline_graph_link_calculation_duration_seconds` | Histogram | 13.9 | Total time spent calculating links, in seconds | |
| `pipeline_graph_links_total` | Histogram | 13.9 | Number of links per graph | |
| `pipeline_graph_links_per_job_ratio` | Histogram | 13.9 | Ratio of links to job per graph | |
| `gitlab_ci_pipeline_security_orchestration_policy_processing_duration_seconds` | Histogram | 13.12 | Time in seconds it takes to process Security Policies in CI/CD pipeline | |
## Metrics controlled by a feature flag

View File

@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
description: 'Learn how to administer GitLab Pages.'
---
# GitLab Pages administration
# GitLab Pages administration **(FREE SELF)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80) in GitLab EE 8.3.
> - Custom CNAMEs with TLS support were [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/173) in GitLab EE 8.5.

View File

@ -4,7 +4,7 @@ group: Release
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/#assignments
---
# GitLab Pages administration for source installations
# GitLab Pages administration for source installations **(FREE SELF)**
NOTE:
Before attempting to enable GitLab Pages, first make sure you have

View File

@ -11817,6 +11817,22 @@ Represents the Geo sync and verification state of a snippet repository.
| <a id="submoduletype"></a>`type` | [`EntryType!`](#entrytype) | Type of tree entry. |
| <a id="submoduleweburl"></a>`webUrl` | [`String`](#string) | Web URL for the sub-module. |
### `Subscription`
#### Fields with arguments
##### `Subscription.issuableAssigneesUpdated`
Triggered when the assignees of an issuable are updated.
Returns [`Issuable`](#issuable).
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="subscriptionissuableassigneesupdatedissuableid"></a>`issuableId` | [`IssuableID!`](#issuableid) | ID of the issuable. |
### `TaskCompletionStatus`
Completion status of tasks.
@ -14181,6 +14197,12 @@ An example `IncidentManagementOncallRotationID` is: `"gid://gitlab/IncidentManag
Represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.
### `IssuableID`
A `IssuableID` is a global ID. It is encoded as a string.
An example `IssuableID` is: `"gid://gitlab/Issuable/1"`.
### `IssueID`
A `IssueID` is a global ID. It is encoded as a string.
@ -14390,6 +14412,16 @@ abstract types.
### Unions
#### `Issuable`
Represents an issuable.
One of:
- [`Epic`](#epic)
- [`Issue`](#issue)
- [`MergeRequest`](#mergerequest)
#### `PackageMetadata`
Represents metadata associated with a Package.

View File

@ -89,8 +89,6 @@ The easiest way to access tracing from a GDK environment is through the
[performance-bar](../administration/monitoring/performance/performance_bar.md). This can be shown
by typing `p` `b` in the browser window.
![Jaeger Search UI](img/distributed_tracing_performance_bar.png)
Once the performance bar is enabled, click on the **Trace** link in the performance bar to go to
the Jaeger UI.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -0,0 +1,133 @@
# frozen_string_literal: true
module API
module Concerns
module Packages
module DebianEndpoints
extend ActiveSupport::Concern
DISTRIBUTION_REGEX = %r{[a-zA-Z0-9][a-zA-Z0-9.-]*}.freeze
COMPONENT_REGEX = %r{[a-z-]+}.freeze
ARCHITECTURE_REGEX = %r{[a-z][a-z0-9]*}.freeze
LETTER_REGEX = %r{(lib)?[a-z0-9]}.freeze
PACKAGE_REGEX = API::NO_SLASH_URL_PART_REGEX
DISTRIBUTION_REQUIREMENTS = {
distribution: DISTRIBUTION_REGEX
}.freeze
COMPONENT_ARCHITECTURE_REQUIREMENTS = {
component: COMPONENT_REGEX,
architecture: ARCHITECTURE_REGEX
}.freeze
COMPONENT_LETTER_SOURCE_PACKAGE_REQUIREMENTS = {
component: COMPONENT_REGEX,
letter: LETTER_REGEX,
source_package: PACKAGE_REGEX
}.freeze
FILE_NAME_REQUIREMENTS = {
file_name: API::NO_SLASH_URL_PART_REGEX
}.freeze
included do
feature_category :package_registry
helpers ::API::Helpers::PackagesHelpers
helpers ::API::Helpers::Packages::BasicAuthHelpers
format :txt
content_type :txt, 'text/plain'
rescue_from ArgumentError do |e|
render_api_error!(e.message, 400)
end
rescue_from ActiveRecord::RecordInvalid do |e|
render_api_error!(e.message, 400)
end
before do
require_packages_enabled!
end
namespace 'packages/debian' do
params do
requires :distribution, type: String, desc: 'The Debian Codename', regexp: Gitlab::Regex.debian_distribution_regex
end
namespace 'dists/*distribution', requirements: DISTRIBUTION_REQUIREMENTS do
# GET {projects|groups}/:id/packages/debian/dists/*distribution/Release.gpg
desc 'The Release file signature' do
detail 'This feature was introduced in GitLab 13.5'
end
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth, authenticate_non_public: true
get 'Release.gpg' do
not_found!
end
# GET {projects|groups}/:id/packages/debian/dists/*distribution/Release
desc 'The unsigned Release file' do
detail 'This feature was introduced in GitLab 13.5'
end
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth, authenticate_non_public: true
get 'Release' do
# https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286
'TODO Release'
end
# GET {projects|groups}/:id/packages/debian/dists/*distribution/InRelease
desc 'The signed Release file' do
detail 'This feature was introduced in GitLab 13.5'
end
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth, authenticate_non_public: true
get 'InRelease' do
not_found!
end
params do
requires :component, type: String, desc: 'The Debian Component', regexp: Gitlab::Regex.debian_component_regex
requires :architecture, type: String, desc: 'The Debian Architecture', regexp: Gitlab::Regex.debian_architecture_regex
end
namespace ':component/binary-:architecture', requirements: COMPONENT_ARCHITECTURE_REQUIREMENTS do
# GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages
desc 'The binary files index' do
detail 'This feature was introduced in GitLab 13.5'
end
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth, authenticate_non_public: true
get 'Packages' do
# https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286
'TODO Packages'
end
end
end
params do
requires :component, type: String, desc: 'The Debian Component', regexp: Gitlab::Regex.debian_component_regex
requires :letter, type: String, desc: 'The Debian Classification (first-letter or lib-first-letter)'
requires :source_package, type: String, desc: 'The Debian Source Package Name', regexp: Gitlab::Regex.debian_package_name_regex
end
namespace 'pool/:component/:letter/:source_package', requirements: COMPONENT_LETTER_SOURCE_PACKAGE_REQUIREMENTS do
# GET {projects|groups}/:id/packages/debian/pool/:component/:letter/:source_package/:file_name
params do
requires :file_name, type: String, desc: 'The Debian File Name'
end
desc 'The package' do
detail 'This feature was introduced in GitLab 13.5'
end
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth, authenticate_non_public: true
get ':file_name', requirements: FILE_NAME_REQUIREMENTS do
# https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286
'TODO File'
end
end
end
end
end
end
end
end

View File

@ -15,8 +15,8 @@ module API
authorize_read_package!(user_group)
end
namespace ':id/-/packages/debian' do
include DebianPackageEndpoints
namespace ':id/-' do
include ::API::Concerns::Packages::DebianEndpoints
end
end
end

View File

@ -1,127 +0,0 @@
# frozen_string_literal: true
module API
module DebianPackageEndpoints
extend ActiveSupport::Concern
DISTRIBUTION_REGEX = %r{[a-zA-Z0-9][a-zA-Z0-9.-]*}.freeze
COMPONENT_REGEX = %r{[a-z-]+}.freeze
ARCHITECTURE_REGEX = %r{[a-z][a-z0-9]*}.freeze
LETTER_REGEX = %r{(lib)?[a-z0-9]}.freeze
PACKAGE_REGEX = API::NO_SLASH_URL_PART_REGEX
DISTRIBUTION_REQUIREMENTS = {
distribution: DISTRIBUTION_REGEX
}.freeze
COMPONENT_ARCHITECTURE_REQUIREMENTS = {
component: COMPONENT_REGEX,
architecture: ARCHITECTURE_REGEX
}.freeze
COMPONENT_LETTER_SOURCE_PACKAGE_REQUIREMENTS = {
component: COMPONENT_REGEX,
letter: LETTER_REGEX,
source_package: PACKAGE_REGEX
}.freeze
FILE_NAME_REQUIREMENTS = {
file_name: API::NO_SLASH_URL_PART_REGEX
}.freeze
included do
feature_category :package_registry
helpers ::API::Helpers::PackagesHelpers
helpers ::API::Helpers::Packages::BasicAuthHelpers
format :txt
content_type :txt, 'text/plain'
rescue_from ArgumentError do |e|
render_api_error!(e.message, 400)
end
rescue_from ActiveRecord::RecordInvalid do |e|
render_api_error!(e.message, 400)
end
before do
require_packages_enabled!
end
params do
requires :distribution, type: String, desc: 'The Debian Codename', regexp: Gitlab::Regex.debian_distribution_regex
end
namespace 'dists/*distribution', requirements: DISTRIBUTION_REQUIREMENTS do
# GET {projects|groups}/:id/packages/debian/dists/*distribution/Release.gpg
desc 'The Release file signature' do
detail 'This feature was introduced in GitLab 13.5'
end
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth, authenticate_non_public: true
get 'Release.gpg' do
not_found!
end
# GET {projects|groups}/:id/packages/debian/dists/*distribution/Release
desc 'The unsigned Release file' do
detail 'This feature was introduced in GitLab 13.5'
end
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth, authenticate_non_public: true
get 'Release' do
# https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286
'TODO Release'
end
# GET {projects|groups}/:id/packages/debian/dists/*distribution/InRelease
desc 'The signed Release file' do
detail 'This feature was introduced in GitLab 13.5'
end
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth, authenticate_non_public: true
get 'InRelease' do
not_found!
end
params do
requires :component, type: String, desc: 'The Debian Component', regexp: Gitlab::Regex.debian_component_regex
requires :architecture, type: String, desc: 'The Debian Architecture', regexp: Gitlab::Regex.debian_architecture_regex
end
namespace ':component/binary-:architecture', requirements: COMPONENT_ARCHITECTURE_REQUIREMENTS do
# GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages
desc 'The binary files index' do
detail 'This feature was introduced in GitLab 13.5'
end
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth, authenticate_non_public: true
get 'Packages' do
# https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286
'TODO Packages'
end
end
end
params do
requires :component, type: String, desc: 'The Debian Component', regexp: Gitlab::Regex.debian_component_regex
requires :letter, type: String, desc: 'The Debian Classification (first-letter or lib-first-letter)'
requires :source_package, type: String, desc: 'The Debian Source Package Name', regexp: Gitlab::Regex.debian_package_name_regex
end
namespace 'pool/:component/:letter/:source_package', requirements: COMPONENT_LETTER_SOURCE_PACKAGE_REQUIREMENTS do
# GET {projects|groups}/:id/packages/debian/pool/:component/:letter/:source_package/:file_name
params do
requires :file_name, type: String, desc: 'The Debian File Name'
end
desc 'The package' do
detail 'This feature was introduced in GitLab 13.5'
end
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth, authenticate_non_public: true
get ':file_name', requirements: FILE_NAME_REQUIREMENTS do
# https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286
'TODO File'
end
end
end
end
end

View File

@ -15,14 +15,14 @@ module API
authorize_read_package!
end
namespace ':id/packages/debian' do
include DebianPackageEndpoints
namespace ':id' do
include ::API::Concerns::Packages::DebianEndpoints
params do
requires :file_name, type: String, desc: 'The file name'
end
namespace ':file_name', requirements: FILE_NAME_REQUIREMENTS do
namespace 'packages/debian/:file_name', requirements: FILE_NAME_REQUIREMENTS do
content_type :json, Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
# PUT {projects|groups}/:id/packages/debian/:file_name

View File

@ -10,19 +10,16 @@ module Banzai
protected
def linkable_attributes
strong_memoize(:linkable_attributes) do
attrs = []
attrs += doc.search('a:not(.gfm)').map do |el|
el.attribute('href')
end
attrs += doc.search('img:not(.gfm), video:not(.gfm), audio:not(.gfm)').flat_map do |el|
[el.attribute('src'), el.attribute('data-src')]
end
attrs.reject do |attr|
attr.blank? || attr.value.start_with?('//')
if Feature.enabled?(:optimize_linkable_attributes, project, default_enabled: :yaml)
# Nokorigi Nodeset#search performs badly for documents with many nodes
#
# Here we store fetched attributes in the shared variable "result"
# This variable is passed through the chain of filters and can be
# accessed by them
result[:linkable_attributes] ||= fetch_linkable_attributes
else
strong_memoize(:linkable_attributes) do
fetch_linkable_attributes
end
end
end
@ -40,6 +37,16 @@ module Banzai
def unescape_and_scrub_uri(uri)
Addressable::URI.unescape(uri).scrub.delete("\0")
end
def fetch_linkable_attributes
attrs = []
attrs += doc.search('a:not(.gfm), img:not(.gfm), video:not(.gfm), audio:not(.gfm)').flat_map do |el|
[el.attribute('href'), el.attribute('src'), el.attribute('data-src')]
end
attrs.reject { |attr| attr.blank? || attr.value.start_with?('//') }
end
end
end
end

View File

@ -15,8 +15,16 @@ module Banzai
def call
return doc if context[:system_note]
linkable_attributes.each do |attr|
process_link_to_upload_attr(attr)
if Feature.enabled?(:optimize_linkable_attributes, project, default_enabled: :yaml)
# We exclude processed upload links from the linkable attributes to
# prevent further modifications by RepositoryLinkFilter
linkable_attributes.reject! do |attr|
process_link_to_upload_attr(attr)
end
else
linkable_attributes.each do |attr|
process_link_to_upload_attr(attr)
end
end
doc

View File

@ -62,7 +62,7 @@ class FileSizeValidator < ActiveModel::EachValidator
default_message = options[MESSAGES[key]]
errors_options[:message] ||= default_message if default_message
record.errors.add(attribute, MESSAGES[key], errors_options)
record.errors.add(attribute, MESSAGES[key], **errors_options)
end
end

View File

@ -10,7 +10,7 @@ module Gitlab
attr_reader :location
validates :location, inclusion: { in: %i[http_basic_auth http_token] }
validates :location, inclusion: { in: %i[http_basic_auth http_token token_param] }
def initialize(location)
@location = location
@ -23,6 +23,8 @@ module Gitlab
extract_from_http_basic_auth request
when :http_token
extract_from_http_token request
when :token_param
extract_from_token_param request
end
end
@ -41,6 +43,13 @@ module Gitlab
UsernameAndPassword.new(nil, password)
end
def extract_from_token_param(request)
password = request.query_parameters['token']
return unless password.present?
UsernameAndPassword.new(nil, password)
end
end
end
end

View File

@ -15,9 +15,14 @@ module Gitlab
personal_access_token
job_token
deploy_token
personal_access_token_from_jwt
deploy_token_from_jwt
job_token_from_jwt
]
}
UsernameAndPassword = ::Gitlab::APIAuthentication::TokenLocator::UsernameAndPassword
def initialize(token_type)
@token_type = token_type
validate!
@ -56,6 +61,15 @@ module Gitlab
when :deploy_token_with_username
resolve_deploy_token_with_username raw
when :personal_access_token_from_jwt
resolve_personal_access_token_from_jwt raw
when :deploy_token_from_jwt
resolve_deploy_token_from_jwt raw
when :job_token_from_jwt
resolve_job_token_from_jwt raw
end
end
@ -116,6 +130,33 @@ module Gitlab
end
end
def resolve_personal_access_token_from_jwt(raw)
with_jwt_token(raw) do |jwt_token|
break unless jwt_token['token'].is_a?(Integer)
pat = ::PersonalAccessToken.find(jwt_token['token'])
break unless pat
pat
end
end
def resolve_deploy_token_from_jwt(raw)
with_jwt_token(raw) do |jwt_token|
break unless jwt_token['token'].is_a?(String)
resolve_deploy_token(UsernameAndPassword.new(nil, jwt_token['token']))
end
end
def resolve_job_token_from_jwt(raw)
with_jwt_token(raw) do |jwt_token|
break unless jwt_token['token'].is_a?(String)
resolve_job_token(UsernameAndPassword.new(nil, jwt_token['token']))
end
end
def with_personal_access_token(raw, &block)
pat = ::PersonalAccessToken.find_by_token(raw.password)
return unless pat
@ -136,6 +177,13 @@ module Gitlab
yield(job)
end
def with_jwt_token(raw, &block)
jwt_token = ::Gitlab::JWTToken.decode(raw.password)
raise ::Gitlab::Auth::UnauthorizedError unless jwt_token
yield(jwt_token)
end
end
end
end

View File

@ -13,6 +13,13 @@ module Gitlab
::Gitlab::Metrics.histogram(name, comment, labels, buckets)
end
def self.pipeline_security_orchestration_policy_processing_duration_histogram
name = :gitlab_ci_pipeline_security_orchestration_policy_processing_duration_seconds
comment = 'Pipeline security orchestration policy processing duration'
::Gitlab::Metrics.histogram(name, comment)
end
def self.pipeline_size_histogram
name = :gitlab_ci_pipeline_size_builds
comment = 'Pipeline size'

70
lib/gitlab/jwt_token.rb Normal file
View File

@ -0,0 +1,70 @@
# frozen_string_literal: true
module Gitlab
class JWTToken < JSONWebToken::HMACToken
HMAC_ALGORITHM = 'SHA256'
HMAC_KEY = 'gitlab-jwt'
HMAC_EXPIRES_IN = 5.minutes.freeze
class << self
def decode(jwt)
payload = super(jwt, secret).first
new.tap do |jwt_token|
jwt_token.id = payload.delete('jti')
jwt_token.issued_at = payload.delete('iat')
jwt_token.not_before = payload.delete('nbf')
jwt_token.expire_time = payload.delete('exp')
payload.each do |key, value|
jwt_token[key] = value
end
end
rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::ImmatureSignature => ex
# we want to log and return on expired and errored tokens
Gitlab::ErrorTracking.track_exception(ex)
nil
end
def secret
OpenSSL::HMAC.hexdigest(
HMAC_ALGORITHM,
::Settings.attr_encrypted_db_key_base,
HMAC_KEY
)
end
end
def initialize
super(self.class.secret)
self.expire_time = self.issued_at + HMAC_EXPIRES_IN.to_i
end
def ==(other)
self.id == other.id &&
self.payload == other.payload
end
def issued_at=(value)
super(convert_time(value))
end
def not_before=(value)
super(convert_time(value))
end
def expire_time=(value)
super(convert_time(value))
end
private
def convert_time(value)
# JSONWebToken::Token truncates subsecond precision causing comparisons to
# fail unless we truncate it here first
value = value.to_i if value.is_a?(Float)
value = Time.zone.at(value) if value.is_a?(Integer)
value
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module Gitlab
class TerraformRegistryToken < JWTToken
class << self
def from_token(token)
new.tap do |terraform_registry_token|
terraform_registry_token['token'] = token.try(:token).presence || token.try(:id).presence
end
end
end
end
end

View File

@ -14071,6 +14071,9 @@ msgstr ""
msgid "ForkProject|Internal"
msgstr ""
msgid "ForkProject|Please select a namespace"
msgstr ""
msgid "ForkProject|Private"
msgstr ""
@ -17971,15 +17974,18 @@ msgstr ""
msgid "IssueTracker|Use Redmine as the issue tracker. %{docs_link}"
msgstr ""
msgid "IssueTracker|Use YouTrack as this project's issue tracker."
msgstr ""
msgid "IssueTracker|Use YouTrack as this project's issue tracker. %{docs_link}"
msgstr ""
msgid "IssueTracker|Use a custom issue tracker that is not in the integration list. %{docs_link}"
msgstr ""
msgid "IssueTracker|Use a custom issue tracker."
msgstr ""
msgid "IssueTracker|YouTrack issue tracker"
msgstr ""
msgid "Issues"
msgstr ""

View File

@ -8,7 +8,7 @@ module RuboCop
'https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#type-authorization'
# We want to exclude our own basetypes and scalars
ALLOWED_TYPES = %w[BaseEnum BaseScalar BasePermissionType MutationType
ALLOWED_TYPES = %w[BaseEnum BaseScalar BasePermissionType MutationType SubscriptionType
QueryType GraphQL::Schema BaseUnion BaseInputObject].freeze
def_node_search :authorize?, <<~PATTERN

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
FactoryBot.define do
factory :jwt_token, class: 'Gitlab::JWTToken' do
skip_create
initialize_with { new }
trait :with_custom_payload do
transient do
custom_payload { {} }
end
after(:build) do |jwt, evaluator|
evaluator.custom_payload.each do |key, value|
jwt[key] = value
end
end
end
end
end

View File

@ -6,7 +6,7 @@ RSpec.describe 'Project fork' do
include ProjectForksHelper
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:project) { create(:project, :public, :repository, description: 'some description') }
before do
sign_in(user)

View File

@ -1,5 +1,5 @@
import { GlForm, GlFormInputGroup, GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { GlFormInputGroup, GlFormInput, GlForm } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios';
import AxiosMockAdapter from 'axios-mock-adapter';
import { kebabCase } from 'lodash';
@ -43,8 +43,8 @@ describe('ForkForm component', () => {
axiosMock.onGet(DEFAULT_PROPS.endpoint).replyOnce(statusCode, data);
};
const createComponent = (props = {}, data = {}) => {
wrapper = shallowMount(ForkForm, {
const createComponentFactory = (mountFn) => (props = {}, data = {}) => {
wrapper = mountFn(ForkForm, {
provide: {
newGroupPath: 'some/groups/path',
visibilityHelpPath: 'some/visibility/help/path',
@ -65,6 +65,9 @@ describe('ForkForm component', () => {
});
};
const createComponent = createComponentFactory(shallowMount);
const createFullComponent = createComponentFactory(mount);
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
window.gon = {
@ -99,44 +102,6 @@ describe('ForkForm component', () => {
expect(cancelButton.attributes('href')).toBe(projectFullPath);
});
it('make POST request with project param', async () => {
jest.spyOn(axios, 'post');
const namespaceId = 20;
mockGetRequest();
createComponent(
{},
{
selectedNamespace: {
id: namespaceId,
},
},
);
wrapper.find(GlForm).vm.$emit('submit', { preventDefault: () => {} });
const {
projectId,
projectDescription,
projectName,
projectPath,
projectVisibility,
} = DEFAULT_PROPS;
const url = `/api/${GON_API_VERSION}/projects/${projectId}/fork`;
const project = {
description: projectDescription,
id: projectId,
name: projectName,
namespace_id: namespaceId,
path: projectPath,
visibility: projectVisibility,
};
expect(axios.post).toHaveBeenCalledWith(url, project);
});
it('has input with csrf token', () => {
mockGetRequest();
createComponent();
@ -258,9 +223,7 @@ describe('ForkForm component', () => {
projectVisibility: project,
},
{
selectedNamespace: {
visibility: namespace,
},
form: { fields: { namespace: { value: { visibility: namespace } } } },
},
);
@ -274,34 +237,101 @@ describe('ForkForm component', () => {
describe('onSubmit', () => {
beforeEach(() => {
jest.spyOn(urlUtility, 'redirectTo').mockImplementation();
});
it('redirect to POST web_url response', async () => {
const webUrl = `new/fork-project`;
jest.spyOn(axios, 'post').mockResolvedValue({ data: { web_url: webUrl } });
mockGetRequest();
createComponent();
await wrapper.vm.onSubmit();
expect(urlUtility.redirectTo).toHaveBeenCalledWith(webUrl);
createFullComponent(
{},
{
namespaces: MOCK_NAMESPACES_RESPONSE,
form: {
state: true,
},
},
);
});
it('display flash when POST is unsuccessful', async () => {
const dummyError = 'Fork project failed';
const selectedMockNamespaceIndex = 1;
const namespaceId = MOCK_NAMESPACES_RESPONSE[selectedMockNamespaceIndex].id;
jest.spyOn(axios, 'post').mockRejectedValue(dummyError);
const fillForm = async () => {
const namespaceOptions = findForkUrlInput().findAll('option');
mockGetRequest();
createComponent();
await namespaceOptions.at(selectedMockNamespaceIndex + 1).setSelected();
};
await wrapper.vm.onSubmit();
const submitForm = async () => {
await fillForm();
const form = wrapper.find(GlForm);
expect(urlUtility.redirectTo).not.toHaveBeenCalled();
expect(createFlash).toHaveBeenCalledWith({
message: dummyError,
await form.trigger('submit');
await wrapper.vm.$nextTick();
};
describe('with invalid form', () => {
it('does not make POST request', async () => {
jest.spyOn(axios, 'post');
expect(axios.post).not.toHaveBeenCalled();
});
it('does not redirect the current page', async () => {
await submitForm();
expect(urlUtility.redirectTo).not.toHaveBeenCalled();
});
});
describe('with valid form', () => {
beforeEach(() => {
fillForm();
});
it('make POST request with project param', async () => {
jest.spyOn(axios, 'post');
await submitForm();
const {
projectId,
projectDescription,
projectName,
projectPath,
projectVisibility,
} = DEFAULT_PROPS;
const url = `/api/${GON_API_VERSION}/projects/${projectId}/fork`;
const project = {
description: projectDescription,
id: projectId,
name: projectName,
namespace_id: namespaceId,
path: projectPath,
visibility: projectVisibility,
};
expect(axios.post).toHaveBeenCalledWith(url, project);
});
it('redirect to POST web_url response', async () => {
const webUrl = `new/fork-project`;
jest.spyOn(axios, 'post').mockResolvedValue({ data: { web_url: webUrl } });
await submitForm();
expect(urlUtility.redirectTo).toHaveBeenCalledWith(webUrl);
});
it('display flash when POST is unsuccessful', async () => {
const dummyError = 'Fork project failed';
jest.spyOn(axios, 'post').mockRejectedValue(dummyError);
await submitForm();
expect(urlUtility.redirectTo).not.toHaveBeenCalled();
expect(createFlash).toHaveBeenCalledWith({
message: dummyError,
});
});
});
});

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GraphqlTriggers do
describe '.issuable_assignees_updated' do
it 'triggers the issuableAssigneesUpdated subscription' do
assignees = create_list(:user, 2)
issue = create(:issue, assignees: assignees)
expect(GitlabSchema.subscriptions).to receive(:trigger).with(
'issuableAssigneesUpdated',
{ issuable_id: issue.to_gid },
issue
)
GraphqlTriggers.issuable_assignees_updated(issue)
end
end
end

View File

@ -0,0 +1,68 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Subscriptions::IssuableUpdated do
include GraphqlHelpers
it { expect(described_class).to have_graphql_arguments(:issuable_id) }
it { expect(described_class.payload_type).to eq(Types::IssuableType) }
describe '#resolve' do
let_it_be(:unauthorized_user) { create(:user) }
let_it_be(:issue) { create(:issue) }
let(:current_user) { issue.author }
let(:issuable_id) { issue.to_gid }
subject { resolver.resolve_with_support(issuable_id: issuable_id) }
context 'initial subscription' do
let(:resolver) { resolver_instance(described_class, ctx: { current_user: current_user }, subscription_update: false) }
it 'returns nil' do
expect(subject).to eq(nil)
end
context 'when user is unauthorized' do
let(:current_user) { unauthorized_user }
it 'raises an exception' do
expect { subject }.to raise_error(GraphQL::ExecutionError)
end
end
context 'when issue does not exist' do
let(:issuable_id) { GlobalID.parse("gid://gitlab/Issue/#{non_existing_record_id}") }
it 'raises an exception' do
expect { subject }.to raise_error(GraphQL::ExecutionError)
end
end
context 'when a GraphQL::ID_TYPE is provided' do
let(:issuable_id) { issue.to_gid.to_s }
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
end
end
end
context 'subscription updates' do
let(:resolver) { resolver_instance(described_class, obj: issue, ctx: { current_user: current_user }, subscription_update: true) }
it 'returns the resolved object' do
expect(subject).to eq(issue)
end
context 'when user is unauthorized' do
let(:current_user) { unauthorized_user }
it 'unsubscribes the user' do
expect { subject }.to throw_symbol(:graphql_subscription_unsubscribed)
end
end
end
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['Issuable'] do
it 'returns possible types' do
expect(described_class.possible_types).to include(Types::IssueType, Types::MergeRequestType)
end
describe '.resolve_type' do
it 'resolves issues' do
expect(described_class.resolve_type(build(:issue), {})).to eq(Types::IssueType)
end
it 'resolves merge requests' do
expect(described_class.resolve_type(build(:merge_request), {})).to eq(Types::MergeRequestType)
end
it 'raises an error for invalid types' do
expect { described_class.resolve_type(build(:user), {}) }.to raise_error 'Unsupported issuable type'
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['Subscription'] do
it 'has the expected fields' do
expected_fields = %i[
issuable_assignees_updated
]
expect(described_class).to have_graphql_fields(*expected_fields).only
end
end

View File

@ -3,24 +3,56 @@
require 'spec_helper'
RSpec.describe Banzai::Pipeline::PostProcessPipeline do
subject { described_class.call(doc, context) }
let_it_be(:project) { create(:project, :public, :repository) }
let(:context) { { project: project, ref: 'master' } }
context 'when a document only has upload links' do
let(:doc) do
<<-HTML.strip_heredoc
<a href="/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg">Relative Upload Link</a>
<img src="/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg">
HTML
end
it 'does not make any Gitaly calls', :request_store do
markdown = <<-MARKDOWN.strip_heredoc
[Relative Upload Link](/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg)
![Relative Upload Image](/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg)
MARKDOWN
context = {
project: create(:project, :public, :repository),
ref: 'master'
}
Gitlab::GitalyClient.reset_counts
described_class.call(markdown, context)
subject
expect(Gitlab::GitalyClient.get_request_count).to eq(0)
end
end
context 'when both upload and repository links are present' do
let(:html) do
<<-HTML.strip_heredoc
<a href="/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg">Relative Upload Link</a>
<img src="/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg">
<a href="/test.jpg">Just a link</a>
HTML
end
let(:doc) { HTML::Pipeline.parse(html) }
it 'searches for attributes only once' do
expect(doc).to receive(:search).once.and_call_original
subject
end
context 'when "optimize_linkable_attributes" is disabled' do
before do
stub_feature_flags(optimize_linkable_attributes: false)
end
it 'searches for attributes twice' do
expect(doc).to receive(:search).twice.and_call_original
subject
end
end
end
end

View File

@ -36,7 +36,7 @@ RSpec.describe Gitlab::APIAuthentication::TokenLocator do
let(:request) { double(authorization: nil) }
it 'returns nil' do
expect(subject).to be(nil)
expect(subject).to be_nil
end
end
@ -59,7 +59,7 @@ RSpec.describe Gitlab::APIAuthentication::TokenLocator do
let(:request) { double(headers: {}) }
it 'returns nil' do
expect(subject).to be(nil)
expect(subject).to be_nil
end
end
@ -72,5 +72,26 @@ RSpec.describe Gitlab::APIAuthentication::TokenLocator do
end
end
end
context 'with :token_param' do
let(:type) { :token_param }
context 'without credentials' do
let(:request) { double(query_parameters: {}) }
it 'returns nil' do
expect(subject).to be_nil
end
end
context 'with credentials' do
let(:password) { 'bar' }
let(:request) { double(query_parameters: { 'token' => password }) }
it 'returns the credentials' do
expect(subject.password).to eq(password)
end
end
end
end
end

View File

@ -160,9 +160,58 @@ RSpec.describe Gitlab::APIAuthentication::TokenResolver do
it_behaves_like 'an authorized request'
end
end
context 'with :personal_access_token_from_jwt' do
let(:type) { :personal_access_token_from_jwt }
let(:token) { personal_access_token }
context 'with valid credentials' do
let(:raw) { username_and_password_from_jwt(token.id) }
it_behaves_like 'an authorized request'
end
end
context 'with :deploy_token_from_jwt' do
let(:type) { :deploy_token_from_jwt }
let(:token) { deploy_token }
context 'with valid credentials' do
let(:raw) { username_and_password_from_jwt(token.token) }
it_behaves_like 'an authorized request'
end
end
context 'with :job_token_from_jwt' do
let(:type) { :job_token_from_jwt }
let(:token) { ci_job }
context 'with valid credentials' do
let(:raw) { username_and_password_from_jwt(token.token) }
it_behaves_like 'an authorized request'
end
context 'when the job is not running' do
let(:raw) { username_and_password_from_jwt(ci_job_done.token) }
it_behaves_like 'an unauthorized request'
end
context 'with an invalid job token' do
let(:raw) { username_and_password_from_jwt('not a valid CI job token') }
it_behaves_like 'an unauthorized request'
end
end
end
def username_and_password(username, password)
::Gitlab::APIAuthentication::TokenLocator::UsernameAndPassword.new(username, password)
end
def username_and_password_from_jwt(token)
username_and_password(nil, ::Gitlab::JWTToken.new.tap { |jwt| jwt['token'] = token }.encoded)
end
end

View File

@ -74,7 +74,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::Deployments do
it 'adds an informative error to the pipeline' do
perform
expect(pipeline.errors.messages).to include(base: ['Pipeline has too many deployments! Requested 2, but the limit is 1.'])
expect(pipeline.errors.added?(:base, 'Pipeline has too many deployments! Requested 2, but the limit is 1.')).to be true
end
it 'increments the error metric' do

View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::JWTToken do
it_behaves_like 'a gitlab jwt token'
end

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::TerraformRegistryToken do
let_it_be(:user) { create(:user) }
describe '.from_token' do
let(:jwt_token) { described_class.from_token(token) }
subject { described_class.decode(jwt_token.encoded) }
context 'with a deploy token' do
let(:deploy_token) { create(:deploy_token, username: 'deployer') }
let(:token) { deploy_token }
it 'returns the correct token' do
expect(subject['token']).to eq jwt_token['token']
end
end
context 'with a job' do
let_it_be(:job) { create(:ci_build) }
let(:token) { job }
it 'returns the correct token' do
expect(subject['token']).to eq jwt_token['token']
end
end
context 'with a personal access token' do
let(:token) { create(:personal_access_token) }
it 'returns the correct token' do
expect(subject['token']).to eq jwt_token['token']
end
end
end
it_behaves_like 'a gitlab jwt token'
end

View File

@ -56,8 +56,7 @@ RSpec.shared_examples 'ChronicDurationAttribute writer' do
subject.send("#{virtual_field}=", '-10m')
expect(subject.valid?).to be_falsey
expect(subject.errors&.messages)
.to include(base: ['Maximum job timeout has a value which could not be accepted'])
expect(subject.errors.added?(:base, 'Maximum job timeout has a value which could not be accepted')).to be true
end
end

View File

@ -38,7 +38,7 @@ RSpec.describe CustomEmoji do
new_emoji = build(:custom_emoji, name: old_emoji.name, namespace: old_emoji.namespace, group: group)
expect(new_emoji).not_to be_valid
expect(new_emoji.errors.messages).to include(name: ["has already been taken"])
expect(new_emoji.errors.messages).to eq(creator: ["can't be blank"], name: ["has already been taken"])
end
it 'disallows non http and https file value' do

View File

@ -579,7 +579,9 @@ RSpec.describe Group do
it "is false if avatar is html page" do
group.update_attribute(:avatar, 'uploads/avatar.html')
expect(group.avatar_type).to eq(["file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp"])
group.avatar_type
expect(group.errors.added?(:avatar, "file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp")).to be true
end
end

View File

@ -23,7 +23,7 @@ RSpec.describe MergeRequestDiff do
expect(subject.valid?).to be false
expect(subject.errors.count).to eq 3
expect(subject.errors).to all(include('is not a valid SHA'))
expect(subject.errors.full_messages).to all(include('is not a valid SHA'))
end
it 'does not validate uniqueness by default' do

View File

@ -1276,7 +1276,9 @@ RSpec.describe Project, factory_default: :keep do
it 'is false if avatar is html page' do
project.update_attribute(:avatar, 'uploads/avatar.html')
expect(project.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp'])
project.avatar_type
expect(project.errors.added?(:avatar, "file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp")).to be true
end
end

View File

@ -2526,8 +2526,9 @@ RSpec.describe User do
it 'is false if avatar is html page' do
user.update_attribute(:avatar, 'uploads/avatar.html')
user.avatar_type
expect(user.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp'])
expect(user.errors.added?(:avatar, "file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp")).to be true
end
end

View File

@ -1014,13 +1014,15 @@ RSpec.describe Issues::UpdateService, :mailer do
with_them do
it 'broadcasts to the issues channel based on ActionCable and feature flag values' do
expect(Gitlab::ActionCable::Config).to receive(:in_app?).and_return(action_cable_in_app_enabled)
allow(Gitlab::ActionCable::Config).to receive(:in_app?).and_return(action_cable_in_app_enabled)
stub_feature_flags(broadcast_issue_updates: feature_flag_enabled)
if should_broadcast
expect(IssuesChannel).to receive(:broadcast_to).with(issue, event: 'updated')
expect(GraphqlTriggers).to receive(:issuable_assignees_updated).with(issue)
else
expect(IssuesChannel).not_to receive(:broadcast_to)
expect(GraphqlTriggers).not_to receive(:issuable_assignees_updated).with(issue)
end
update_issue(update_params)

View File

@ -173,7 +173,7 @@ RSpec.describe Notes::BuildService do
let(:user) { create(:user) }
it 'returns `Discussion to reply to cannot be found` error' do
expect(new_note.errors.first).to include("Discussion to reply to cannot be found")
expect(new_note.errors.added?(:base, "Discussion to reply to cannot be found")).to be true
end
end
end

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Packages::Debian::GenerateDistributionKeyService do
let_it_be(:user) { create(:user) }
let(:params) { {} }
subject { described_class.new(current_user: user, params: params) }
let(:response) { subject.execute }
context 'with a user' do
it 'returns an Hash', :aggregate_failures do
expect(GPGME::Ctx).to receive(:new).with(armor: true, offline: true).and_call_original
expect(User).to receive(:random_password).with(no_args).and_call_original
expect(response).to be_a Hash
expect(response.keys).to contain_exactly(:private_key, :public_key, :fingerprint, :passphrase)
expect(response[:private_key]).to start_with('-----BEGIN PGP PRIVATE KEY BLOCK-----')
expect(response[:public_key]).to start_with('-----BEGIN PGP PUBLIC KEY BLOCK-----')
expect(response[:fingerprint].length).to eq(40)
expect(response[:passphrase].length).to be > 10
end
end
context 'without a user' do
let(:user) { nil }
it 'raises an ArgumentError' do
expect { response }.to raise_error(ArgumentError, 'Please provide a user')
end
end
end

View File

@ -142,9 +142,9 @@ module GraphqlHelpers
Class.new(::Types::BaseObject) { graphql_name name }
end
def resolver_instance(resolver_class, obj: nil, ctx: {}, field: nil, schema: GitlabSchema)
def resolver_instance(resolver_class, obj: nil, ctx: {}, field: nil, schema: GitlabSchema, subscription_update: false)
if ctx.is_a?(Hash)
q = double('Query', schema: schema)
q = double('Query', schema: schema, subscription_update?: subscription_update)
ctx = GraphQL::Query::Context.new(query: q, object: obj, values: ctx)
end

View File

@ -0,0 +1,49 @@
# frozen_string_literal: true
RSpec.shared_examples 'a gitlab jwt token' do
let_it_be(:base_secret) { SecureRandom.base64(64) }
let(:jwt_secret) do
OpenSSL::HMAC.hexdigest(
'SHA256',
base_secret,
described_class::HMAC_KEY
)
end
before do
allow(Settings).to receive(:attr_encrypted_db_key_base).and_return(base_secret)
end
describe '#secret' do
subject { described_class.secret }
it { is_expected.to eq(jwt_secret) }
end
describe '#decode' do
let(:encoded_jwt_token) { jwt_token.encoded }
subject(:decoded_jwt_token) { described_class.decode(encoded_jwt_token) }
context 'with a custom payload' do
let(:personal_access_token) { create(:personal_access_token) }
let(:jwt_token) { described_class.new.tap { |jwt_token| jwt_token['token'] = personal_access_token.token } }
it 'returns the correct token' do
expect(decoded_jwt_token['token']).to eq jwt_token['token']
end
it 'returns nil and logs the exception after expiration' do
travel_to((described_class::HMAC_EXPIRES_IN + 1.minute).ago) do
encoded_jwt_token
end
expect(Gitlab::ErrorTracking).to receive(:track_exception)
.with(instance_of(JWT::ExpiredSignature))
expect(decoded_jwt_token).to be_nil
end
end
end
end

View File

@ -19,18 +19,20 @@ RSpec.describe AddressableUrlValidator do
it 'returns error when url is nil' do
expect(validator.validate_each(badge, :link_url, nil)).to be_falsey
expect(badge.errors.first[1]).to eq validator.options.fetch(:message)
expect(badge.errors.added?(:link_url, validator.options.fetch(:message))).to be true
end
it 'returns error when url is empty' do
expect(validator.validate_each(badge, :link_url, '')).to be_falsey
expect(badge.errors.first[1]).to eq validator.options.fetch(:message)
expect(badge.errors.added?(:link_url, validator.options.fetch(:message))).to be true
end
it 'does not allow urls with CR or LF characters' do
aggregate_failures do
urls_with_CRLF.each do |url|
expect(validator.validate_each(badge, :link_url, url)[0]).to eq 'is blocked: URI is invalid'
validator.validate_each(badge, :link_url, url)
expect(badge.errors.added?(:link_url, 'is blocked: URI is invalid')).to be true
end
end
end
@ -113,7 +115,7 @@ RSpec.describe AddressableUrlValidator do
it 'does block nil url with provided error message' do
expect(validator.validate_each(badge, :link_url, nil)).to be_falsey
expect(badge.errors.first[1]).to eq message
expect(badge.errors.added?(:link_url, message)).to be true
end
end
@ -126,7 +128,7 @@ RSpec.describe AddressableUrlValidator do
subject
expect(badge.errors.first[1]).to eq 'is not allowed due to: Only allowed schemes are http, https'
expect(badge.errors.added?(:link_url, 'is not allowed due to: Only allowed schemes are http, https')).to be true
end
end

View File

@ -49,7 +49,7 @@ RSpec.describe ArrayMembersValidator do
object = test_class.new(children: [])
expect(object.valid?).to be_falsey
expect(object.errors.messages).to eql(children: ['should be an array of children objects'])
expect(object.errors.messages).to eq(children: ['should be an array of children objects'])
end
end
@ -62,7 +62,7 @@ RSpec.describe ArrayMembersValidator do
object = test_class.new(children: [])
expect(object.valid?).to be_falsey
expect(object.errors.messages).to eql(children: ['should be an array of test objects'])
expect(object.errors.messages).to eq(children: ['should be an array of test objects'])
end
end
end

View File

@ -23,7 +23,7 @@ RSpec.describe DeviseEmailValidator do
subject
expect(user.errors).to be_present
expect(user.errors.first[1]).to eq 'is invalid'
expect(user.errors.added?(:public_email)).to be true
end
it 'returns error when email is nil' do
@ -40,7 +40,7 @@ RSpec.describe DeviseEmailValidator do
subject
expect(user.errors).to be_present
expect(user.errors.first[1]).to eq 'is invalid'
expect(user.errors.added?(:public_email)).to be true
end
end
end

View File

@ -21,7 +21,7 @@ RSpec.describe Gitlab::Utils::ZoomUrlValidator do
expect(zoom_meeting.valid?).to eq(false)
expect(zoom_meeting.errors).to be_present
expect(zoom_meeting.errors.first[1]).to eq 'must contain one valid Zoom URL'
expect(zoom_meeting.errors.added?(:url, 'must contain one valid Zoom URL')).to be true
end
end

View File

@ -52,7 +52,7 @@ RSpec.describe QualifiedDomainArrayValidator do
subject
expect(record.errors).to be_present
expect(record.errors.first[1]).to eq('entries cannot be nil')
expect(record.errors.added?(:domain_array, "entries cannot be nil")).to be true
end
it 'allows when domain is valid' do
@ -67,7 +67,7 @@ RSpec.describe QualifiedDomainArrayValidator do
subject
expect(record.errors).to be_present
expect(record.errors.first[1]).to eq 'unicode domains should use IDNA encoding'
expect(record.errors.added?(:domain_array, 'unicode domains should use IDNA encoding')).to be true
end
it 'returns error when entry is larger than 255 chars' do
@ -76,7 +76,7 @@ RSpec.describe QualifiedDomainArrayValidator do
subject
expect(record.errors).to be_present
expect(record.errors.first[1]).to eq 'entries cannot be larger than 255 characters'
expect(record.errors.added?(:domain_array, 'entries cannot be larger than 255 characters')).to be true
end
it 'returns error when entry contains HTML tags' do
@ -85,7 +85,7 @@ RSpec.describe QualifiedDomainArrayValidator do
subject
expect(record.errors).to be_present
expect(record.errors.first[1]).to eq 'entries cannot contain HTML tags'
expect(record.errors.added?(:domain_array, 'entries cannot contain HTML tags')).to be true
end
end