New upstream version 14.6.5+ds1
This commit is contained in:
parent
1beab03f69
commit
ae28033eb9
43 changed files with 652 additions and 71 deletions
|
@ -113,7 +113,7 @@
|
|||
policy: push
|
||||
|
||||
.qa-ruby-gems-cache: &qa-ruby-gems-cache
|
||||
key: "qa-ruby-gems-v1"
|
||||
key: "qa-ruby-gems-v1-debian-buster"
|
||||
paths:
|
||||
- qa/vendor/ruby/
|
||||
policy: pull
|
||||
|
|
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -2,6 +2,19 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 14.6.5 (2022-02-25)
|
||||
|
||||
### Security (8 changes)
|
||||
|
||||
- [Limit commands_changes to certain keys](gitlab-org/security/gitlab@138c437f2819d62ce4750fb84399d8868c844b01) ([merge request](gitlab-org/security/gitlab!2227))
|
||||
- [Add runners_token prefix to Group and Project](gitlab-org/security/gitlab@682d4e9b63d3d36901638edc75c1b265460d42dc) ([merge request](gitlab-org/security/gitlab!2250))
|
||||
- [Anonymous user can enumerate all users through GraphQL endpoint](gitlab-org/security/gitlab@2b00a8036b291d3ad5de551a5e13c2a0a39d0234) ([merge request](gitlab-org/security/gitlab!2102))
|
||||
- [Check for unsafe characters in email addresses before sending](gitlab-org/security/gitlab@6bc653b3dadefb3d2c80823786d43e6b7f8c4620) ([merge request](gitlab-org/security/gitlab!2208))
|
||||
- [Warn when snippet contains unretrievable files](gitlab-org/security/gitlab@f9ae9515ec98ab934f4aa3a35af0aca806bbe21d) ([merge request](gitlab-org/security/gitlab!2203))
|
||||
- [Prevent DOS when rendering math markdown](gitlab-org/security/gitlab@fd6d496df6f4b5eb3da0b851f9ff8ebb1d68d3f2) ([merge request](gitlab-org/security/gitlab!2201))
|
||||
- [Check permission when creating members through service](gitlab-org/security/gitlab@948e5103285de2a6cdb5152ff2c13ae4db2f4cda) ([merge request](gitlab-org/security/gitlab!2211))
|
||||
- [Reset password field on page load](gitlab-org/security/gitlab@1417b463f2771a4b17e068dea9de3aa6c4540962) ([merge request](gitlab-org/security/gitlab!2194))
|
||||
|
||||
## 14.6.4 (2022-02-03)
|
||||
|
||||
No changes.
|
||||
|
|
|
@ -1 +1 @@
|
|||
14.6.4
|
||||
14.6.5
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
14.6.4
|
||||
14.6.5
|
|
@ -6,6 +6,8 @@ import { __ } from '~/locale';
|
|||
import { hide } from '~/tooltips';
|
||||
import SSHMirror from './ssh_mirror';
|
||||
|
||||
const PASSWORD_FIELD_SELECTOR = '.js-mirror-password-field';
|
||||
|
||||
export default class MirrorRepos {
|
||||
constructor(container) {
|
||||
this.$container = $(container);
|
||||
|
@ -27,7 +29,6 @@ export default class MirrorRepos {
|
|||
this.$passwordGroup = $('.js-password-group', this.$container);
|
||||
this.$password = $('.js-password', this.$passwordGroup);
|
||||
this.$authMethod = $('.js-auth-method', this.$form);
|
||||
|
||||
this.$keepDivergentRefsInput.on('change', () => this.updateKeepDivergentRefs());
|
||||
this.$authMethod.on('change', () => this.togglePassword());
|
||||
this.$password.on('input.updateUrl', () => this.debouncedUpdateUrl());
|
||||
|
@ -35,6 +36,13 @@ export default class MirrorRepos {
|
|||
this.initMirrorSSH();
|
||||
this.updateProtectedBranches();
|
||||
this.updateKeepDivergentRefs();
|
||||
MirrorRepos.resetPasswordField();
|
||||
}
|
||||
|
||||
static resetPasswordField() {
|
||||
if (document.querySelector(PASSWORD_FIELD_SELECTOR)) {
|
||||
document.querySelector(PASSWORD_FIELD_SELECTOR).value = '';
|
||||
}
|
||||
}
|
||||
|
||||
initMirrorSSH() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
|
||||
import eventHub from '~/blob/components/eventhub';
|
||||
import {
|
||||
SNIPPET_MARK_VIEW_APP_START,
|
||||
|
@ -23,6 +23,7 @@ export default {
|
|||
EmbedDropdown,
|
||||
SnippetHeader,
|
||||
SnippetTitle,
|
||||
GlAlert,
|
||||
GlLoadingIcon,
|
||||
SnippetBlob,
|
||||
CloneDropdownButton,
|
||||
|
@ -35,6 +36,9 @@ export default {
|
|||
canBeCloned() {
|
||||
return Boolean(this.snippet.sshUrlToRepo || this.snippet.httpUrlToRepo);
|
||||
},
|
||||
hasUnretrievableBlobs() {
|
||||
return this.snippet.hasUnretrievableBlobs;
|
||||
},
|
||||
},
|
||||
beforeCreate() {
|
||||
performanceMarkAndMeasure({ mark: SNIPPET_MARK_VIEW_APP_START });
|
||||
|
@ -66,6 +70,13 @@ export default {
|
|||
data-qa-selector="clone_button"
|
||||
/>
|
||||
</div>
|
||||
<gl-alert v-if="hasUnretrievableBlobs" variant="danger" class="gl-mb-3" :dismissible="false">
|
||||
{{
|
||||
__(
|
||||
'WARNING: This snippet contains hidden files which might be used to mask malicious behavior. Exercise caution if cloning and executing code from this snippet.',
|
||||
)
|
||||
}}
|
||||
</gl-alert>
|
||||
<snippet-blob
|
||||
v-for="blob in blobs"
|
||||
:key="blob.path"
|
||||
|
|
|
@ -17,6 +17,7 @@ export const getSnippetMixin = {
|
|||
|
||||
// Set `snippet.blobs` since some child components are coupled to this.
|
||||
if (!isEmpty(res)) {
|
||||
res.hasUnretrievableBlobs = res.blobs?.hasUnretrievableBlobs || false;
|
||||
// It's possible for us to not get any blobs in a response.
|
||||
// In this case, we should default to current blobs.
|
||||
res.blobs = res.blobs ? res.blobs.nodes : blobsDefault;
|
||||
|
|
|
@ -15,6 +15,7 @@ query GetSnippetQuery($ids: [SnippetID!]) {
|
|||
sshUrlToRepo
|
||||
blobs {
|
||||
__typename
|
||||
hasUnretrievableBlobs
|
||||
nodes {
|
||||
__typename
|
||||
binary
|
||||
|
|
|
@ -12,6 +12,7 @@ query SnippetBlobContent($ids: [ID!], $rich: Boolean!, $paths: [String!]) {
|
|||
richData @include(if: $rich)
|
||||
plainData @skip(if: $rich)
|
||||
}
|
||||
hasUnretrievableBlobs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,18 +19,18 @@ module Resolvers
|
|||
def resolve(paths: [])
|
||||
return [snippet.blob] if snippet.empty_repo?
|
||||
|
||||
if paths.empty?
|
||||
snippet.blobs
|
||||
else
|
||||
snippet.repository.blobs_at(transformed_blob_paths(paths))
|
||||
end
|
||||
end
|
||||
paths = snippet.all_files if paths.empty?
|
||||
blobs = snippet.blobs(paths)
|
||||
|
||||
private
|
||||
# TODO: Some blobs, e.g. those with non-utf8 filenames, are returned as nil from the
|
||||
# repository. We need to provide a flag to notify the user of this until we come up with a
|
||||
# way to retrieve and display these blobs. We will be exploring a more holistic solution for
|
||||
# this general problem of making all blobs retrievable as part
|
||||
# of https://gitlab.com/gitlab-org/gitlab/-/issues/323082, at which point this attribute may
|
||||
# be removed.
|
||||
context[:unretrievable_blobs?] = blobs.size < paths.size
|
||||
|
||||
def transformed_blob_paths(paths)
|
||||
ref = snippet.default_branch
|
||||
paths.map { |path| [ref, path] }
|
||||
blobs
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,7 +29,7 @@ module Resolvers
|
|||
description: 'Return only admin users.'
|
||||
|
||||
def resolve(ids: nil, usernames: nil, sort: nil, search: nil, admins: nil)
|
||||
authorize!
|
||||
authorize!(usernames)
|
||||
|
||||
::UsersFinder.new(context[:current_user], finder_params(ids, usernames, sort, search, admins)).execute
|
||||
end
|
||||
|
@ -46,8 +46,11 @@ module Resolvers
|
|||
super
|
||||
end
|
||||
|
||||
def authorize!
|
||||
Ability.allowed?(context[:current_user], :read_users_list) || raise_resource_not_available_error!
|
||||
def authorize!(usernames)
|
||||
authorized = Ability.allowed?(context[:current_user], :read_users_list)
|
||||
authorized &&= usernames.present? if context[:current_user].blank?
|
||||
|
||||
raise_resource_not_available_error! unless authorized
|
||||
end
|
||||
|
||||
private
|
||||
|
|
16
app/graphql/types/snippets/blob_connection_type.rb
Normal file
16
app/graphql/types/snippets/blob_connection_type.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module Snippets
|
||||
# rubocop: disable Graphql/AuthorizeTypes
|
||||
class BlobConnectionType < GraphQL::Types::Relay::BaseConnection
|
||||
field :has_unretrievable_blobs, GraphQL::Types::Boolean, null: false,
|
||||
description: 'Indicates if the snippet has unretrievable blobs.',
|
||||
resolver_method: :unretrievable_blobs?
|
||||
|
||||
def unretrievable_blobs?
|
||||
!!context[:unretrievable_blobs?]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -8,6 +8,8 @@ module Types
|
|||
description 'Represents the snippet blob'
|
||||
present_using SnippetBlobPresenter
|
||||
|
||||
connection_type_class(Types::Snippets::BlobConnectionType)
|
||||
|
||||
field :rich_data, GraphQL::Types::String,
|
||||
description: 'Blob highlighted data.',
|
||||
null: true
|
||||
|
|
|
@ -5,7 +5,7 @@ module TokenAuthenticatableStrategies
|
|||
def find_token_authenticatable(token, unscoped = false)
|
||||
return if token.blank?
|
||||
|
||||
if required?
|
||||
instance = if required?
|
||||
find_by_encrypted_token(token, unscoped)
|
||||
elsif optional?
|
||||
find_by_encrypted_token(token, unscoped) ||
|
||||
|
@ -15,6 +15,8 @@ module TokenAuthenticatableStrategies
|
|||
else
|
||||
raise ArgumentError, _("Unknown encryption strategy: %{encrypted_strategy}!") % { encrypted_strategy: encrypted_strategy }
|
||||
end
|
||||
|
||||
instance if instance && matches_prefix?(instance, token)
|
||||
end
|
||||
|
||||
def ensure_token(instance)
|
||||
|
@ -41,9 +43,7 @@ module TokenAuthenticatableStrategies
|
|||
def get_token(instance)
|
||||
return insecure_strategy.get_token(instance) if migrating?
|
||||
|
||||
encrypted_token = instance.read_attribute(encrypted_field)
|
||||
token = EncryptionHelper.decrypt_token(encrypted_token)
|
||||
token || (insecure_strategy.get_token(instance) if optional?)
|
||||
get_encrypted_token(instance)
|
||||
end
|
||||
|
||||
def set_token(instance, token)
|
||||
|
@ -69,6 +69,12 @@ module TokenAuthenticatableStrategies
|
|||
|
||||
protected
|
||||
|
||||
def get_encrypted_token(instance)
|
||||
encrypted_token = instance.read_attribute(encrypted_field)
|
||||
token = EncryptionHelper.decrypt_token(encrypted_token)
|
||||
token || (insecure_strategy.get_token(instance) if optional?)
|
||||
end
|
||||
|
||||
def encrypted_strategy
|
||||
value = options[:encrypted]
|
||||
value = value.call if value.is_a?(Proc)
|
||||
|
@ -95,14 +101,22 @@ module TokenAuthenticatableStrategies
|
|||
.new(klass, token_field, options)
|
||||
end
|
||||
|
||||
def token_set?(instance)
|
||||
raw_token = instance.read_attribute(encrypted_field)
|
||||
def matches_prefix?(instance, token)
|
||||
prefix = options[:prefix]
|
||||
prefix = prefix.call(instance) if prefix.is_a?(Proc)
|
||||
prefix = '' unless prefix.is_a?(String)
|
||||
|
||||
unless required?
|
||||
raw_token ||= insecure_strategy.get_token(instance)
|
||||
token.start_with?(prefix)
|
||||
end
|
||||
|
||||
raw_token.present?
|
||||
def token_set?(instance)
|
||||
token = get_encrypted_token(instance)
|
||||
|
||||
unless required?
|
||||
token ||= insecure_strategy.get_token(instance)
|
||||
end
|
||||
|
||||
token.present? && matches_prefix?(instance, token)
|
||||
end
|
||||
|
||||
def encrypted_field
|
||||
|
|
|
@ -18,6 +18,13 @@ class Group < Namespace
|
|||
include EachBatch
|
||||
include BulkMemberAccessLoad
|
||||
|
||||
extend ::Gitlab::Utils::Override
|
||||
|
||||
# Prefix for runners_token which can be used to invalidate existing tokens.
|
||||
# The value chosen here is GR (for Gitlab Runner) combined with the rotation
|
||||
# date (20220225) decimal to hex encoded.
|
||||
RUNNERS_TOKEN_PREFIX = 'GR1348941'
|
||||
|
||||
def self.sti_name
|
||||
'Group'
|
||||
end
|
||||
|
@ -108,7 +115,9 @@ class Group < Namespace
|
|||
message: Gitlab::Regex.group_name_regex_message },
|
||||
if: :name_changed?
|
||||
|
||||
add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
|
||||
add_authentication_token_field :runners_token,
|
||||
encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
|
||||
prefix: ->(instance) { instance.runners_token_prefix }
|
||||
|
||||
after_create :post_create_hook
|
||||
after_destroy :post_destroy_hook
|
||||
|
@ -652,6 +661,15 @@ class Group < Namespace
|
|||
ensure_runners_token!
|
||||
end
|
||||
|
||||
def runners_token_prefix
|
||||
Feature.enabled?(:groups_runners_token_prefix, self, default_enabled: :yaml) ? RUNNERS_TOKEN_PREFIX : ''
|
||||
end
|
||||
|
||||
override :format_runners_token
|
||||
def format_runners_token(token)
|
||||
"#{runners_token_prefix}#{token}"
|
||||
end
|
||||
|
||||
def project_creation_level
|
||||
super || ::Gitlab::CurrentSettings.default_project_creation
|
||||
end
|
||||
|
|
|
@ -46,7 +46,7 @@ class Note < ApplicationRecord
|
|||
attr_accessor :user_visible_reference_count
|
||||
|
||||
# Attribute used to store the attributes that have been changed by quick actions.
|
||||
attr_accessor :commands_changes
|
||||
attr_writer :commands_changes
|
||||
|
||||
# Attribute used to determine whether keep_around_commits will be skipped for diff notes.
|
||||
attr_accessor :skip_keep_around_commits
|
||||
|
@ -612,6 +612,41 @@ class Note < ApplicationRecord
|
|||
change_position.line_range["end"] || change_position.line_range["start"]
|
||||
end
|
||||
|
||||
def commands_changes
|
||||
@commands_changes&.slice(
|
||||
:due_date,
|
||||
:label_ids,
|
||||
:remove_label_ids,
|
||||
:add_label_ids,
|
||||
:canonical_issue_id,
|
||||
:clone_with_notes,
|
||||
:confidential,
|
||||
:create_merge_request,
|
||||
:add_contacts,
|
||||
:remove_contacts,
|
||||
:assignee_ids,
|
||||
:milestone_id,
|
||||
:time_estimate,
|
||||
:spend_time,
|
||||
:discussion_locked,
|
||||
:merge,
|
||||
:rebase,
|
||||
:wip_event,
|
||||
:target_branch,
|
||||
:reviewer_ids,
|
||||
:health_status,
|
||||
:promote_to_epic,
|
||||
:weight,
|
||||
:emoji_award,
|
||||
:todo_event,
|
||||
:subscription_event,
|
||||
:state_event,
|
||||
:title,
|
||||
:tag_message,
|
||||
:tag_name
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def system_note_viewable_by?(user)
|
||||
|
|
|
@ -73,6 +73,11 @@ class Project < ApplicationRecord
|
|||
|
||||
GL_REPOSITORY_TYPES = [Gitlab::GlRepository::PROJECT, Gitlab::GlRepository::WIKI, Gitlab::GlRepository::DESIGN].freeze
|
||||
|
||||
# Prefix for runners_token which can be used to invalidate existing tokens.
|
||||
# The value chosen here is GR (for Gitlab Runner) combined with the rotation
|
||||
# date (20220225) decimal to hex encoded.
|
||||
RUNNERS_TOKEN_PREFIX = 'GR1348941'
|
||||
|
||||
cache_markdown_field :description, pipeline: :description
|
||||
|
||||
default_value_for :packages_enabled, true
|
||||
|
@ -93,7 +98,9 @@ class Project < ApplicationRecord
|
|||
default_value_for :autoclose_referenced_issues, true
|
||||
default_value_for(:ci_config_path) { Gitlab::CurrentSettings.default_ci_config_path }
|
||||
|
||||
add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
|
||||
add_authentication_token_field :runners_token,
|
||||
encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
|
||||
prefix: ->(instance) { instance.runners_token_prefix }
|
||||
|
||||
before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
|
||||
|
||||
|
@ -1843,6 +1850,15 @@ class Project < ApplicationRecord
|
|||
ensure_runners_token!
|
||||
end
|
||||
|
||||
def runners_token_prefix
|
||||
Feature.enabled?(:projects_runners_token_prefix, self, default_enabled: :yaml) ? RUNNERS_TOKEN_PREFIX : ''
|
||||
end
|
||||
|
||||
override :format_runners_token
|
||||
def format_runners_token(token)
|
||||
"#{runners_token_prefix}#{token}"
|
||||
end
|
||||
|
||||
def pages_deployed?
|
||||
pages_metadatum&.deployed?
|
||||
end
|
||||
|
|
|
@ -237,15 +237,19 @@ class Snippet < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def all_files
|
||||
list_files(default_branch)
|
||||
end
|
||||
|
||||
def blob
|
||||
@blob ||= Blob.decorate(SnippetBlob.new(self), self)
|
||||
end
|
||||
|
||||
def blobs
|
||||
def blobs(paths = [])
|
||||
return [] unless repository_exists?
|
||||
|
||||
files = list_files(default_branch)
|
||||
items = files.map { |file| [default_branch, file] }
|
||||
paths = all_files if paths.empty?
|
||||
items = paths.map { |path| [default_branch, path] }
|
||||
|
||||
repository.blobs_at(items).compact
|
||||
end
|
||||
|
|
|
@ -19,6 +19,8 @@ module Members
|
|||
end
|
||||
|
||||
def execute
|
||||
raise Gitlab::Access::AccessDeniedError unless can?(current_user, create_member_permission(source), source)
|
||||
|
||||
validate_invite_source!
|
||||
validate_invitable!
|
||||
|
||||
|
@ -144,6 +146,17 @@ module Members
|
|||
def formatted_errors
|
||||
errors.to_sentence
|
||||
end
|
||||
|
||||
def create_member_permission(source)
|
||||
case source
|
||||
when Group
|
||||
:admin_group_member
|
||||
when Project
|
||||
:admin_project_member
|
||||
else
|
||||
raise "Unknown source type: #{source.class}!"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -13,4 +13,4 @@
|
|||
.form-group
|
||||
.well-password-auth.collapse.js-well-password-auth
|
||||
= f.label :password, _("Password"), class: "label-bold"
|
||||
= f.password_field :password, class: 'form-control gl-form-input qa-password', autocomplete: 'new-password'
|
||||
= f.password_field :password, class: 'form-control gl-form-input qa-password js-mirror-password-field', autocomplete: 'off'
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: groups_runners_token_prefix
|
||||
introduced_by_url:
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353805
|
||||
milestone: '14.9'
|
||||
type: development
|
||||
group: group::database
|
||||
default_enabled: true
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: projects_runners_token_prefix
|
||||
introduced_by_url:
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353805
|
||||
milestone: '14.9'
|
||||
type: development
|
||||
group: group::database
|
||||
default_enabled: true
|
|
@ -8,6 +8,7 @@ end
|
|||
ActionMailer::Base.register_interceptors(
|
||||
::Gitlab::Email::Hook::AdditionalHeadersInterceptor,
|
||||
::Gitlab::Email::Hook::EmailTemplateInterceptor,
|
||||
::Gitlab::Email::Hook::ValidateAddressesInterceptor,
|
||||
::Gitlab::Email::Hook::DeliveryMetricsObserver
|
||||
)
|
||||
|
||||
|
|
|
@ -7519,6 +7519,7 @@ The connection type for [`SnippetBlob`](#snippetblob).
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="snippetblobconnectionedges"></a>`edges` | [`[SnippetBlobEdge]`](#snippetblobedge) | A list of edges. |
|
||||
| <a id="snippetblobconnectionhasunretrievableblobs"></a>`hasUnretrievableBlobs` | [`Boolean!`](#boolean) | Indicates if the snippet has unretrievable blobs. |
|
||||
| <a id="snippetblobconnectionnodes"></a>`nodes` | [`[SnippetBlob]`](#snippetblob) | A list of nodes. |
|
||||
| <a id="snippetblobconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
|
||||
|
||||
|
|
|
@ -105,9 +105,6 @@ module API
|
|||
params.except!(:created_after, :created_before, :order_by, :sort, :two_factor, :without_projects)
|
||||
end
|
||||
|
||||
users = UsersFinder.new(current_user, params).execute
|
||||
users = reorder_users(users)
|
||||
|
||||
authorized = can?(current_user, :read_users_list)
|
||||
|
||||
# When `current_user` is not present, require that the `username`
|
||||
|
@ -119,6 +116,9 @@ module API
|
|||
|
||||
forbidden!("Not authorized to access /api/v4/users") unless authorized
|
||||
|
||||
users = UsersFinder.new(current_user, params).execute
|
||||
users = reorder_users(users)
|
||||
|
||||
entity = current_user&.admin? ? Entities::UserWithAdmin : Entities::UserBasic
|
||||
users = users.preload(:identities, :u2f_registrations) if entity == Entities::UserWithAdmin
|
||||
users = users.preload(:identities, :webauthn_registrations) if entity == Entities::UserWithAdmin
|
||||
|
|
|
@ -25,7 +25,14 @@ module Banzai
|
|||
|
||||
DOLLAR_SIGN = '$'
|
||||
|
||||
# Limit to how many nodes can be marked as math elements.
|
||||
# Prevents timeouts for large notes.
|
||||
# For more information check: https://gitlab.com/gitlab-org/gitlab/-/issues/341832
|
||||
RENDER_NODES_LIMIT = 50
|
||||
|
||||
def call
|
||||
nodes_count = 0
|
||||
|
||||
doc.xpath(XPATH_CODE).each do |code|
|
||||
closing = code.next
|
||||
opening = code.previous
|
||||
|
@ -41,6 +48,9 @@ module Banzai
|
|||
code[STYLE_ATTRIBUTE] = 'inline'
|
||||
closing.content = closing.content[1..]
|
||||
opening.content = opening.content[0..-2]
|
||||
|
||||
nodes_count += 1
|
||||
break if nodes_count >= RENDER_NODES_LIMIT
|
||||
end
|
||||
end
|
||||
|
||||
|
|
32
lib/gitlab/email/hook/validate_addresses_interceptor.rb
Normal file
32
lib/gitlab/email/hook/validate_addresses_interceptor.rb
Normal file
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Email
|
||||
module Hook
|
||||
# Check for unsafe characters in the envelope-from and -to addresses.
|
||||
# These are passed directly as arguments to sendmail and are liable to shell injection attacks:
|
||||
# https://github.com/mikel/mail/blob/2.7.1/lib/mail/network/delivery_methods/sendmail.rb#L53-L58
|
||||
class ValidateAddressesInterceptor
|
||||
UNSAFE_CHARACTERS = /(\\|[^[:print:]])/.freeze
|
||||
|
||||
def self.delivering_email(message)
|
||||
addresses = Array(message.smtp_envelope_from) + Array(message.smtp_envelope_to)
|
||||
|
||||
addresses.each do |address|
|
||||
next unless address.match?(UNSAFE_CHARACTERS)
|
||||
|
||||
Gitlab::AuthLogger.info(
|
||||
message: 'Skipping email with unsafe characters in address',
|
||||
address: address,
|
||||
subject: message.subject
|
||||
)
|
||||
|
||||
message.perform_deliveries = false
|
||||
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -39328,6 +39328,9 @@ msgstr ""
|
|||
msgid "WARNING:"
|
||||
msgstr ""
|
||||
|
||||
msgid "WARNING: This snippet contains hidden files which might be used to mask malicious behavior. Exercise caution if cloning and executing code from this snippet."
|
||||
msgstr ""
|
||||
|
||||
msgid "Wait for the file to load to copy its contents"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
|
||||
import EmbedDropdown from '~/snippets/components/embed_dropdown.vue';
|
||||
|
@ -106,6 +106,23 @@ describe('Snippet view app', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('hasUnretrievableBlobs alert rendering', () => {
|
||||
it.each`
|
||||
hasUnretrievableBlobs | condition | isRendered
|
||||
${false} | ${'not render'} | ${false}
|
||||
${true} | ${'render'} | ${true}
|
||||
`('does $condition gl-alert by default', ({ hasUnretrievableBlobs, isRendered }) => {
|
||||
createComponent({
|
||||
data: {
|
||||
snippet: {
|
||||
hasUnretrievableBlobs,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(wrapper.findComponent(GlAlert).exists()).toBe(isRendered);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Clone button rendering', () => {
|
||||
it.each`
|
||||
httpUrlToRepo | sshUrlToRepo | shouldRender | isRendered
|
||||
|
|
|
@ -13,11 +13,14 @@ RSpec.describe Resolvers::Snippets::BlobsResolver do
|
|||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:snippet) { create(:personal_snippet, :private, :repository, author: current_user) }
|
||||
|
||||
let(:query_context) { {} }
|
||||
|
||||
context 'when user is not authorized' do
|
||||
let(:other_user) { create(:user) }
|
||||
|
||||
it 'redacts the field' do
|
||||
expect(resolve_blobs(snippet, user: other_user)).to be_nil
|
||||
expect(query_context[:unretrievable_blobs?]).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -28,6 +31,7 @@ RSpec.describe Resolvers::Snippets::BlobsResolver do
|
|||
expect(result).to match_array(snippet.list_files.map do |file|
|
||||
have_attributes(path: file)
|
||||
end)
|
||||
expect(query_context[:unretrievable_blobs?]).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -37,12 +41,14 @@ RSpec.describe Resolvers::Snippets::BlobsResolver do
|
|||
path = 'CHANGELOG'
|
||||
|
||||
expect(resolve_blobs(snippet, paths: [path])).to contain_exactly(have_attributes(path: path))
|
||||
expect(query_context[:unretrievable_blobs?]).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'the argument does not match anything' do
|
||||
it 'returns an empty result' do
|
||||
expect(resolve_blobs(snippet, paths: ['does not exist'])).to be_empty
|
||||
expect(query_context[:unretrievable_blobs?]).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -53,12 +59,15 @@ RSpec.describe Resolvers::Snippets::BlobsResolver do
|
|||
expect(resolve_blobs(snippet, paths: paths)).to match_array(paths.map do |file|
|
||||
have_attributes(path: file)
|
||||
end)
|
||||
expect(query_context[:unretrievable_blobs?]).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def resolve_blobs(snippet, user: current_user, paths: [], args: { paths: paths })
|
||||
resolve(described_class, args: args, ctx: { current_user: user }, obj: snippet)
|
||||
def resolve_blobs(snippet, user: current_user, paths: [], args: { paths: paths }, has_unretrievable_blobs: false)
|
||||
query_context[:current_user] = user
|
||||
query_context[:unretrievable_blobs?] = has_unretrievable_blobs
|
||||
resolve(described_class, args: args, ctx: query_context, obj: snippet)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,6 +7,7 @@ RSpec.describe Resolvers::UsersResolver do
|
|||
|
||||
let_it_be(:user1) { create(:user, name: "SomePerson") }
|
||||
let_it_be(:user2) { create(:user, username: "someone123784") }
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
|
||||
specify do
|
||||
expect(described_class).to have_nullable_graphql_type(Types::UserType.connection_type)
|
||||
|
@ -14,14 +15,14 @@ RSpec.describe Resolvers::UsersResolver do
|
|||
|
||||
describe '#resolve' do
|
||||
it 'raises an error when read_users_list is not authorized' do
|
||||
expect(Ability).to receive(:allowed?).with(nil, :read_users_list).and_return(false)
|
||||
expect(Ability).to receive(:allowed?).with(current_user, :read_users_list).and_return(false)
|
||||
|
||||
expect { resolve_users }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||
end
|
||||
|
||||
context 'when no arguments are passed' do
|
||||
it 'returns all users' do
|
||||
expect(resolve_users).to contain_exactly(user1, user2)
|
||||
expect(resolve_users).to contain_exactly(user1, user2, current_user)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -65,9 +66,21 @@ RSpec.describe Resolvers::UsersResolver do
|
|||
expect(resolve_users( args: { search: "someperson" } )).to contain_exactly(user1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with anonymous access' do
|
||||
let_it_be(:current_user) { nil }
|
||||
|
||||
it 'prohibits search without usernames passed' do
|
||||
expect { resolve_users }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||
end
|
||||
|
||||
it 'allows to search by username' do
|
||||
expect(resolve_users(args: { usernames: [user1.username] })).to contain_exactly(user1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def resolve_users(args: {}, ctx: {})
|
||||
resolve(described_class, args: args, ctx: ctx)
|
||||
resolve(described_class, args: args, ctx: { current_user: current_user }.merge(ctx))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -126,4 +126,12 @@ RSpec.describe Banzai::Filter::MathFilter do
|
|||
expect(before.to_s).to eq '$'
|
||||
expect(after.to_s).to eq '$'
|
||||
end
|
||||
|
||||
it 'limits how many elements can be marked as math' do
|
||||
stub_const('Banzai::Filter::MathFilter::RENDER_NODES_LIMIT', 2)
|
||||
|
||||
doc = filter('$<code>2+2</code>$ + $<code>3+3</code>$ + $<code>4+4</code>$')
|
||||
|
||||
expect(doc.search('.js-render-math').count).to eq(2)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Email::Hook::ValidateAddressesInterceptor do
|
||||
describe 'UNSAFE_CHARACTERS' do
|
||||
subject { described_class::UNSAFE_CHARACTERS }
|
||||
|
||||
it { is_expected.to match('\\') }
|
||||
it { is_expected.to match("\x00") }
|
||||
it { is_expected.to match("\x01") }
|
||||
it { is_expected.not_to match('') }
|
||||
it { is_expected.not_to match('user@example.com') }
|
||||
it { is_expected.not_to match('foo-123+bar_456@example.com') }
|
||||
end
|
||||
|
||||
describe '.delivering_email' do
|
||||
let(:mail) do
|
||||
ActionMailer::Base.mail(to: 'test@mail.com', from: 'info@mail.com', subject: 'title', body: 'hello')
|
||||
end
|
||||
|
||||
let(:unsafe_email) { "evil+\x01$HOME@example.com" }
|
||||
|
||||
it 'sends emails to normal addresses' do
|
||||
expect(Gitlab::AuthLogger).not_to receive(:info)
|
||||
expect { mail.deliver_now }.to change(ActionMailer::Base.deliveries, :count)
|
||||
end
|
||||
|
||||
[:from, :to, :cc, :bcc].each do |header|
|
||||
it "does not send emails if the #{header.inspect} header contains unsafe characters" do
|
||||
mail[header] = unsafe_email
|
||||
|
||||
expect(Gitlab::AuthLogger).to receive(:info).with(
|
||||
message: 'Skipping email with unsafe characters in address',
|
||||
address: unsafe_email,
|
||||
subject: mail.subject
|
||||
)
|
||||
|
||||
expect { mail.deliver_now }.not_to change(ActionMailer::Base.deliveries, :count)
|
||||
end
|
||||
end
|
||||
|
||||
[:reply_to].each do |header|
|
||||
it "sends emails if the #{header.inspect} header contains unsafe characters" do
|
||||
mail[header] = unsafe_email
|
||||
|
||||
expect(Gitlab::AuthLogger).not_to receive(:info)
|
||||
expect { mail.deliver_now }.to change(ActionMailer::Base.deliveries, :count)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -290,3 +290,106 @@ RSpec.describe Ci::Build, 'TokenAuthenticatable' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'prefixed token rotation' do
|
||||
describe "ensure_runners_token" do
|
||||
subject { instance.ensure_runners_token }
|
||||
|
||||
context 'token is not set' do
|
||||
it 'generates a new token' do
|
||||
expect(subject).to match(/^#{instance.class::RUNNERS_TOKEN_PREFIX}/)
|
||||
expect(instance).not_to be_persisted
|
||||
end
|
||||
end
|
||||
|
||||
context 'token is set, but does not match the prefix' do
|
||||
before do
|
||||
instance.set_runners_token('abcdef')
|
||||
end
|
||||
|
||||
it 'generates a new token' do
|
||||
expect(subject).to match(/^#{instance.class::RUNNERS_TOKEN_PREFIX}/)
|
||||
expect(instance).not_to be_persisted
|
||||
end
|
||||
|
||||
context 'feature flag is disabled' do
|
||||
before do
|
||||
flag = "#{described_class.name.downcase.pluralize}_runners_token_prefix"
|
||||
stub_feature_flags(flag => false)
|
||||
end
|
||||
|
||||
it 'leaves the token unchanged' do
|
||||
expect { subject }.not_to change(instance, :runners_token)
|
||||
expect(instance).not_to be_persisted
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'token is set and matches prefix' do
|
||||
before do
|
||||
instance.set_runners_token(instance.class::RUNNERS_TOKEN_PREFIX + '-abcdef')
|
||||
end
|
||||
|
||||
it 'leaves the token unchanged' do
|
||||
expect { subject }.not_to change(instance, :runners_token)
|
||||
expect(instance).not_to be_persisted
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'ensure_runners_token!' do
|
||||
subject { instance.ensure_runners_token! }
|
||||
|
||||
context 'token is not set' do
|
||||
it 'generates a new token' do
|
||||
expect(subject).to match(/^#{instance.class::RUNNERS_TOKEN_PREFIX}/)
|
||||
expect(instance).to be_persisted
|
||||
end
|
||||
end
|
||||
|
||||
context 'token is set, but does not match the prefix' do
|
||||
before do
|
||||
instance.set_runners_token('abcdef')
|
||||
end
|
||||
|
||||
it 'generates a new token' do
|
||||
expect(subject).to match(/^#{instance.class::RUNNERS_TOKEN_PREFIX}/)
|
||||
expect(instance).to be_persisted
|
||||
end
|
||||
|
||||
context 'feature flag is disabled' do
|
||||
before do
|
||||
flag = "#{described_class.name.downcase.pluralize}_runners_token_prefix"
|
||||
stub_feature_flags(flag => false)
|
||||
end
|
||||
|
||||
it 'leaves the token unchanged' do
|
||||
expect { subject }.not_to change(instance, :runners_token)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'token is set and matches prefix' do
|
||||
before do
|
||||
instance.set_runners_token(instance.class::RUNNERS_TOKEN_PREFIX + '-abcdef')
|
||||
instance.save!
|
||||
end
|
||||
|
||||
it 'leaves the token unchanged' do
|
||||
expect { subject }.not_to change(instance, :runners_token)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe Project, 'TokenAuthenticatable' do
|
||||
let(:instance) { build(:project, runners_token: nil) }
|
||||
|
||||
it_behaves_like 'prefixed token rotation'
|
||||
end
|
||||
|
||||
RSpec.describe Group, 'TokenAuthenticatable' do
|
||||
let(:instance) { build(:group, runners_token: nil) }
|
||||
|
||||
it_behaves_like 'prefixed token rotation'
|
||||
end
|
||||
|
|
|
@ -30,6 +30,21 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
|
|||
expect(subject.find_token_authenticatable('my-value'))
|
||||
.to eq 'encrypted resource'
|
||||
end
|
||||
|
||||
context 'when a prefix is required' do
|
||||
let(:options) { { encrypted: :required, prefix: 'GR1348941' } }
|
||||
|
||||
it 'finds the encrypted resource by cleartext' do
|
||||
allow(model).to receive(:where)
|
||||
.and_return(model)
|
||||
allow(model).to receive(:find_by)
|
||||
.with('some_field_encrypted' => [encrypted, encrypted_with_static_iv])
|
||||
.and_return('encrypted resource')
|
||||
|
||||
expect(subject.find_token_authenticatable('my-value'))
|
||||
.to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when encryption is optional' do
|
||||
|
@ -56,6 +71,21 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
|
|||
expect(subject.find_token_authenticatable('my-value'))
|
||||
.to eq 'plaintext resource'
|
||||
end
|
||||
|
||||
context 'when a prefix is required' do
|
||||
let(:options) { { encrypted: :optional, prefix: 'GR1348941' } }
|
||||
|
||||
it 'finds the encrypted resource by cleartext' do
|
||||
allow(model).to receive(:where)
|
||||
.and_return(model)
|
||||
allow(model).to receive(:find_by)
|
||||
.with('some_field_encrypted' => [encrypted, encrypted_with_static_iv])
|
||||
.and_return('encrypted resource')
|
||||
|
||||
expect(subject.find_token_authenticatable('my-value'))
|
||||
.to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when encryption is migrating' do
|
||||
|
@ -78,6 +108,21 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
|
|||
expect(subject.find_token_authenticatable('my-value'))
|
||||
.to be_nil
|
||||
end
|
||||
|
||||
context 'when a prefix is required' do
|
||||
let(:options) { { encrypted: :migrating, prefix: 'GR1348941' } }
|
||||
|
||||
it 'finds the encrypted resource by cleartext' do
|
||||
allow(model).to receive(:where)
|
||||
.and_return(model)
|
||||
allow(model).to receive(:find_by)
|
||||
.with('some_field' => 'my-value')
|
||||
.and_return('cleartext resource')
|
||||
|
||||
expect(subject.find_token_authenticatable('my-value'))
|
||||
.to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -2775,4 +2775,12 @@ RSpec.describe Group do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#runners_token' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
subject { group }
|
||||
|
||||
it_behaves_like 'it has a prefixable runners_token', :groups_runners_token_prefix
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1624,4 +1624,14 @@ RSpec.describe Note do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#commands_changes' do
|
||||
let(:note) { build(:note) }
|
||||
|
||||
it 'only returns allowed keys' do
|
||||
note.commands_changes = { emoji_award: {}, time_estimate: {}, spend_time: {}, target_project: build(:project) }
|
||||
|
||||
expect(note.commands_changes.keys).to contain_exactly(:emoji_award, :time_estimate, :spend_time)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -765,8 +765,8 @@ RSpec.describe Project, factory_default: :keep do
|
|||
end
|
||||
|
||||
it 'does not set an random token if one provided' do
|
||||
project = FactoryBot.create(:project, runners_token: 'my-token')
|
||||
expect(project.runners_token).to eq('my-token')
|
||||
project = FactoryBot.create(:project, runners_token: "#{Project::RUNNERS_TOKEN_PREFIX}my-token")
|
||||
expect(project.runners_token).to eq("#{Project::RUNNERS_TOKEN_PREFIX}my-token")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -7551,6 +7551,14 @@ RSpec.describe Project, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#runners_token' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
subject { project }
|
||||
|
||||
it_behaves_like 'it has a prefixable runners_token', :projects_runners_token_prefix
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def finish_job(export_job)
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Snippet do
|
||||
include FakeBlobHelpers
|
||||
|
||||
describe 'modules' do
|
||||
subject { described_class }
|
||||
|
||||
|
@ -526,6 +528,21 @@ RSpec.describe Snippet do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#all_files' do
|
||||
let(:snippet) { create(:snippet, :repository) }
|
||||
let(:files) { double(:files) }
|
||||
|
||||
subject(:all_files) { snippet.all_files }
|
||||
|
||||
before do
|
||||
allow(snippet.repository).to receive(:ls_files).with(snippet.default_branch).and_return(files)
|
||||
end
|
||||
|
||||
it 'lists files from the repository with the default branch' do
|
||||
expect(all_files).to eq(files)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#blobs' do
|
||||
context 'when repository does not exist' do
|
||||
let(:snippet) { create(:snippet) }
|
||||
|
@ -552,6 +569,23 @@ RSpec.describe Snippet do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when some blobs are not retrievable from repository' do
|
||||
let(:snippet) { create(:snippet, :repository) }
|
||||
let(:container) { double(:container) }
|
||||
let(:retrievable_filename) { 'retrievable_file'}
|
||||
let(:unretrievable_filename) { 'unretrievable_file'}
|
||||
|
||||
before do
|
||||
allow(snippet).to receive(:list_files).and_return([retrievable_filename, unretrievable_filename])
|
||||
blob = fake_blob(path: retrievable_filename, container: container)
|
||||
allow(snippet.repository).to receive(:blobs_at).and_return([blob, nil])
|
||||
end
|
||||
|
||||
it 'does not include unretrievable blobs' do
|
||||
expect(snippet.blobs.map(&:name)).to contain_exactly(retrievable_filename)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#to_json' do
|
||||
|
|
|
@ -5,11 +5,13 @@ require 'spec_helper'
|
|||
RSpec.describe 'Users' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:current_user) { create(:user, created_at: 1.day.ago) }
|
||||
let_it_be(:user0) { create(:user, created_at: 1.day.ago) }
|
||||
let_it_be(:user1) { create(:user, created_at: 2.days.ago) }
|
||||
let_it_be(:user2) { create(:user, created_at: 3.days.ago) }
|
||||
let_it_be(:user3) { create(:user, created_at: 4.days.ago) }
|
||||
|
||||
let(:current_user) { user0 }
|
||||
|
||||
describe '.users' do
|
||||
shared_examples 'a working users query' do
|
||||
it_behaves_like 'a working graphql query' do
|
||||
|
@ -19,7 +21,7 @@ RSpec.describe 'Users' do
|
|||
end
|
||||
|
||||
it 'includes a list of users' do
|
||||
post_graphql(query)
|
||||
post_graphql(query, current_user: current_user)
|
||||
|
||||
expect(graphql_data.dig('users', 'nodes')).not_to be_empty
|
||||
end
|
||||
|
@ -47,7 +49,7 @@ RSpec.describe 'Users' do
|
|||
let_it_be(:query) { graphql_query_for(:users, { ids: user1.to_global_id.to_s, usernames: user1.username }, 'nodes { id }') }
|
||||
|
||||
it 'displays an error' do
|
||||
post_graphql(query)
|
||||
post_graphql(query, current_user: current_user)
|
||||
|
||||
expect(graphql_errors).to include(
|
||||
a_hash_including('message' => a_string_matching(%r{Provide either a list of usernames or ids}))
|
||||
|
@ -66,14 +68,14 @@ RSpec.describe 'Users' do
|
|||
|
||||
it_behaves_like 'a working users query'
|
||||
|
||||
it 'includes all non-admin users', :aggregate_failures do
|
||||
post_graphql(query)
|
||||
it 'includes all users', :aggregate_failures do
|
||||
post_query
|
||||
|
||||
expect(graphql_data.dig('users', 'nodes')).to include(
|
||||
{ "id" => user0.to_global_id.to_s },
|
||||
{ "id" => user1.to_global_id.to_s },
|
||||
{ "id" => user2.to_global_id.to_s },
|
||||
{ "id" => user3.to_global_id.to_s },
|
||||
{ "id" => current_user.to_global_id.to_s },
|
||||
{ "id" => admin.to_global_id.to_s },
|
||||
{ "id" => another_admin.to_global_id.to_s }
|
||||
)
|
||||
|
@ -81,10 +83,12 @@ RSpec.describe 'Users' do
|
|||
end
|
||||
|
||||
context 'when current user is an admin' do
|
||||
let(:current_user) { admin }
|
||||
|
||||
it_behaves_like 'a working users query'
|
||||
|
||||
it 'includes only admins', :aggregate_failures do
|
||||
post_graphql(query, current_user: admin)
|
||||
post_graphql(query, current_user: current_user)
|
||||
|
||||
expect(graphql_data.dig('users', 'nodes')).to include(
|
||||
{ "id" => another_admin.to_global_id.to_s },
|
||||
|
@ -92,10 +96,10 @@ RSpec.describe 'Users' do
|
|||
)
|
||||
|
||||
expect(graphql_data.dig('users', 'nodes')).not_to include(
|
||||
{ "id" => user0.to_global_id.to_s },
|
||||
{ "id" => user1.to_global_id.to_s },
|
||||
{ "id" => user2.to_global_id.to_s },
|
||||
{ "id" => user3.to_global_id.to_s },
|
||||
{ "id" => current_user.to_global_id.to_s }
|
||||
{ "id" => user3.to_global_id.to_s }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -110,7 +114,7 @@ RSpec.describe 'Users' do
|
|||
end
|
||||
|
||||
context 'when sorting by created_at' do
|
||||
let_it_be(:ascending_users) { [user3, user2, user1, current_user].map { |u| global_id_of(u) } }
|
||||
let_it_be(:ascending_users) { [user3, user2, user1, user0].map { |u| global_id_of(u) } }
|
||||
|
||||
context 'when ascending' do
|
||||
it_behaves_like 'sorted paginated query' do
|
||||
|
|
|
@ -233,11 +233,9 @@ RSpec.describe API::Notes do
|
|||
subject { post api(request_path, user), params: { body: request_body } }
|
||||
|
||||
context 'a command only note' do
|
||||
let(:assignee) { create(:user) }
|
||||
let(:request_body) { "/assign #{assignee.to_reference}" }
|
||||
let(:request_body) { "/spend 1h" }
|
||||
|
||||
before do
|
||||
project.add_developer(assignee)
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
|
@ -256,7 +254,7 @@ RSpec.describe API::Notes do
|
|||
end
|
||||
|
||||
it 'applies the commands' do
|
||||
expect { subject }.to change { merge_request.reset.assignees }
|
||||
expect { subject }.to change { merge_request.reset.total_time_spent }
|
||||
end
|
||||
|
||||
it 'reports the changes' do
|
||||
|
@ -264,9 +262,9 @@ RSpec.describe API::Notes do
|
|||
|
||||
expect(json_response).to include(
|
||||
'commands_changes' => include(
|
||||
'assignee_ids' => [Integer]
|
||||
'spend_time' => include('duration' => 3600)
|
||||
),
|
||||
'summary' => include("Assigned #{assignee.to_reference}.")
|
||||
'summary' => include('Added 1h spent time.')
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,19 +11,37 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
|
|||
|
||||
let(:additional_params) { { invite_source: '_invite_source_' } }
|
||||
let(:params) { { user_ids: user_ids, access_level: access_level }.merge(additional_params) }
|
||||
let(:current_user) { user }
|
||||
|
||||
subject(:execute_service) { described_class.new(user, params.merge({ source: source })).execute }
|
||||
subject(:execute_service) { described_class.new(current_user, params.merge({ source: source })).execute }
|
||||
|
||||
before do
|
||||
if source.is_a?(Project)
|
||||
case source
|
||||
when Project
|
||||
source.add_maintainer(user)
|
||||
OnboardingProgress.onboard(source.namespace)
|
||||
else
|
||||
when Group
|
||||
source.add_owner(user)
|
||||
OnboardingProgress.onboard(source)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the current user does not have permission to create members' do
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
it 'raises a Gitlab::Access::AccessDeniedError' do
|
||||
expect { execute_service }.to raise_error(Gitlab::Access::AccessDeniedError)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when passing an invalid source' do
|
||||
let_it_be(:source) { Object.new }
|
||||
|
||||
it 'raises a RuntimeError' do
|
||||
expect { execute_service }.to raise_error(RuntimeError, 'Unknown source type: Object!')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when passing valid parameters' do
|
||||
it 'adds a user to members' do
|
||||
expect(execute_service[:status]).to eq(:success)
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'it has a prefixable runners_token' do |feature_flag|
|
||||
context 'feature flag enabled' do
|
||||
before do
|
||||
stub_feature_flags(feature_flag => [subject])
|
||||
end
|
||||
|
||||
describe '#runners_token' do
|
||||
it 'has a runners_token_prefix' do
|
||||
expect(subject.runners_token_prefix).not_to be_empty
|
||||
end
|
||||
|
||||
it 'starts with the runners_token_prefix' do
|
||||
expect(subject.runners_token).to start_with(subject.runners_token_prefix)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'feature flag disabled' do
|
||||
before do
|
||||
stub_feature_flags(feature_flag => false)
|
||||
end
|
||||
|
||||
describe '#runners_token' do
|
||||
it 'does not have a runners_token_prefix' do
|
||||
expect(subject.runners_token_prefix).to be_empty
|
||||
end
|
||||
|
||||
it 'starts with the runners_token_prefix' do
|
||||
expect(subject.runners_token).to start_with(subject.runners_token_prefix)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue