Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1165608bfd
commit
65f7976d0c
|
|
@ -1 +1 @@
|
|||
ffbce774bce90b5a65f5b235afe492a7266aa82f
|
||||
16479e5771b69a2b4c22aade7c0a7fc2a6f897ce
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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')
|
||||
|
|
@ -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
|
||||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Reduce number of SQL queries when creating SystemNotes
|
||||
merge_request: 59102
|
||||
author:
|
||||
type: performance
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Review and revise YouTrack integration UI text
|
||||
merge_request: 59998
|
||||
author:
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'Users search: fix avatar size'
|
||||
merge_request: 59883
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||

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

|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::JWTToken do
|
||||
it_behaves_like 'a gitlab jwt token'
|
||||
end
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue