New upstream version 14.6.5+ds1

This commit is contained in:
Pirate Praveen 2022-02-27 12:50:16 +05:30
parent 1beab03f69
commit ae28033eb9
43 changed files with 652 additions and 71 deletions

View file

@ -113,7 +113,7 @@
policy: push policy: push
.qa-ruby-gems-cache: &qa-ruby-gems-cache .qa-ruby-gems-cache: &qa-ruby-gems-cache
key: "qa-ruby-gems-v1" key: "qa-ruby-gems-v1-debian-buster"
paths: paths:
- qa/vendor/ruby/ - qa/vendor/ruby/
policy: pull policy: pull

View file

@ -2,6 +2,19 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. 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) ## 14.6.4 (2022-02-03)
No changes. No changes.

View file

@ -1 +1 @@
14.6.4 14.6.5

View file

@ -1 +1 @@
14.6.4 14.6.5

View file

@ -6,6 +6,8 @@ import { __ } from '~/locale';
import { hide } from '~/tooltips'; import { hide } from '~/tooltips';
import SSHMirror from './ssh_mirror'; import SSHMirror from './ssh_mirror';
const PASSWORD_FIELD_SELECTOR = '.js-mirror-password-field';
export default class MirrorRepos { export default class MirrorRepos {
constructor(container) { constructor(container) {
this.$container = $(container); this.$container = $(container);
@ -27,7 +29,6 @@ export default class MirrorRepos {
this.$passwordGroup = $('.js-password-group', this.$container); this.$passwordGroup = $('.js-password-group', this.$container);
this.$password = $('.js-password', this.$passwordGroup); this.$password = $('.js-password', this.$passwordGroup);
this.$authMethod = $('.js-auth-method', this.$form); this.$authMethod = $('.js-auth-method', this.$form);
this.$keepDivergentRefsInput.on('change', () => this.updateKeepDivergentRefs()); this.$keepDivergentRefsInput.on('change', () => this.updateKeepDivergentRefs());
this.$authMethod.on('change', () => this.togglePassword()); this.$authMethod.on('change', () => this.togglePassword());
this.$password.on('input.updateUrl', () => this.debouncedUpdateUrl()); this.$password.on('input.updateUrl', () => this.debouncedUpdateUrl());
@ -35,6 +36,13 @@ export default class MirrorRepos {
this.initMirrorSSH(); this.initMirrorSSH();
this.updateProtectedBranches(); this.updateProtectedBranches();
this.updateKeepDivergentRefs(); this.updateKeepDivergentRefs();
MirrorRepos.resetPasswordField();
}
static resetPasswordField() {
if (document.querySelector(PASSWORD_FIELD_SELECTOR)) {
document.querySelector(PASSWORD_FIELD_SELECTOR).value = '';
}
} }
initMirrorSSH() { initMirrorSSH() {

View file

@ -1,5 +1,5 @@
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui'; import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import eventHub from '~/blob/components/eventhub'; import eventHub from '~/blob/components/eventhub';
import { import {
SNIPPET_MARK_VIEW_APP_START, SNIPPET_MARK_VIEW_APP_START,
@ -23,6 +23,7 @@ export default {
EmbedDropdown, EmbedDropdown,
SnippetHeader, SnippetHeader,
SnippetTitle, SnippetTitle,
GlAlert,
GlLoadingIcon, GlLoadingIcon,
SnippetBlob, SnippetBlob,
CloneDropdownButton, CloneDropdownButton,
@ -35,6 +36,9 @@ export default {
canBeCloned() { canBeCloned() {
return Boolean(this.snippet.sshUrlToRepo || this.snippet.httpUrlToRepo); return Boolean(this.snippet.sshUrlToRepo || this.snippet.httpUrlToRepo);
}, },
hasUnretrievableBlobs() {
return this.snippet.hasUnretrievableBlobs;
},
}, },
beforeCreate() { beforeCreate() {
performanceMarkAndMeasure({ mark: SNIPPET_MARK_VIEW_APP_START }); performanceMarkAndMeasure({ mark: SNIPPET_MARK_VIEW_APP_START });
@ -66,6 +70,13 @@ export default {
data-qa-selector="clone_button" data-qa-selector="clone_button"
/> />
</div> </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 <snippet-blob
v-for="blob in blobs" v-for="blob in blobs"
:key="blob.path" :key="blob.path"

View file

@ -17,6 +17,7 @@ export const getSnippetMixin = {
// Set `snippet.blobs` since some child components are coupled to this. // Set `snippet.blobs` since some child components are coupled to this.
if (!isEmpty(res)) { if (!isEmpty(res)) {
res.hasUnretrievableBlobs = res.blobs?.hasUnretrievableBlobs || false;
// It's possible for us to not get any blobs in a response. // It's possible for us to not get any blobs in a response.
// In this case, we should default to current blobs. // In this case, we should default to current blobs.
res.blobs = res.blobs ? res.blobs.nodes : blobsDefault; res.blobs = res.blobs ? res.blobs.nodes : blobsDefault;

View file

@ -15,6 +15,7 @@ query GetSnippetQuery($ids: [SnippetID!]) {
sshUrlToRepo sshUrlToRepo
blobs { blobs {
__typename __typename
hasUnretrievableBlobs
nodes { nodes {
__typename __typename
binary binary

View file

@ -12,6 +12,7 @@ query SnippetBlobContent($ids: [ID!], $rich: Boolean!, $paths: [String!]) {
richData @include(if: $rich) richData @include(if: $rich)
plainData @skip(if: $rich) plainData @skip(if: $rich)
} }
hasUnretrievableBlobs
} }
} }
} }

View file

@ -19,18 +19,18 @@ module Resolvers
def resolve(paths: []) def resolve(paths: [])
return [snippet.blob] if snippet.empty_repo? return [snippet.blob] if snippet.empty_repo?
if paths.empty? paths = snippet.all_files if paths.empty?
snippet.blobs blobs = snippet.blobs(paths)
else
snippet.repository.blobs_at(transformed_blob_paths(paths))
end
end
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) blobs
ref = snippet.default_branch
paths.map { |path| [ref, path] }
end end
end end
end end

View file

@ -29,7 +29,7 @@ module Resolvers
description: 'Return only admin users.' description: 'Return only admin users.'
def resolve(ids: nil, usernames: nil, sort: nil, search: nil, admins: nil) 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 ::UsersFinder.new(context[:current_user], finder_params(ids, usernames, sort, search, admins)).execute
end end
@ -46,8 +46,11 @@ module Resolvers
super super
end end
def authorize! def authorize!(usernames)
Ability.allowed?(context[:current_user], :read_users_list) || raise_resource_not_available_error! 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 end
private private

View 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

View file

@ -8,6 +8,8 @@ module Types
description 'Represents the snippet blob' description 'Represents the snippet blob'
present_using SnippetBlobPresenter present_using SnippetBlobPresenter
connection_type_class(Types::Snippets::BlobConnectionType)
field :rich_data, GraphQL::Types::String, field :rich_data, GraphQL::Types::String,
description: 'Blob highlighted data.', description: 'Blob highlighted data.',
null: true null: true

View file

@ -5,16 +5,18 @@ module TokenAuthenticatableStrategies
def find_token_authenticatable(token, unscoped = false) def find_token_authenticatable(token, unscoped = false)
return if token.blank? return if token.blank?
if required? instance = if required?
find_by_encrypted_token(token, unscoped) find_by_encrypted_token(token, unscoped)
elsif optional? elsif optional?
find_by_encrypted_token(token, unscoped) || find_by_encrypted_token(token, unscoped) ||
find_by_plaintext_token(token, unscoped) find_by_plaintext_token(token, unscoped)
elsif migrating? elsif migrating?
find_by_plaintext_token(token, unscoped) find_by_plaintext_token(token, unscoped)
else else
raise ArgumentError, _("Unknown encryption strategy: %{encrypted_strategy}!") % { encrypted_strategy: encrypted_strategy } raise ArgumentError, _("Unknown encryption strategy: %{encrypted_strategy}!") % { encrypted_strategy: encrypted_strategy }
end end
instance if instance && matches_prefix?(instance, token)
end end
def ensure_token(instance) def ensure_token(instance)
@ -41,9 +43,7 @@ module TokenAuthenticatableStrategies
def get_token(instance) def get_token(instance)
return insecure_strategy.get_token(instance) if migrating? return insecure_strategy.get_token(instance) if migrating?
encrypted_token = instance.read_attribute(encrypted_field) get_encrypted_token(instance)
token = EncryptionHelper.decrypt_token(encrypted_token)
token || (insecure_strategy.get_token(instance) if optional?)
end end
def set_token(instance, token) def set_token(instance, token)
@ -69,6 +69,12 @@ module TokenAuthenticatableStrategies
protected 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 def encrypted_strategy
value = options[:encrypted] value = options[:encrypted]
value = value.call if value.is_a?(Proc) value = value.call if value.is_a?(Proc)
@ -95,14 +101,22 @@ module TokenAuthenticatableStrategies
.new(klass, token_field, options) .new(klass, token_field, options)
end end
def matches_prefix?(instance, token)
prefix = options[:prefix]
prefix = prefix.call(instance) if prefix.is_a?(Proc)
prefix = '' unless prefix.is_a?(String)
token.start_with?(prefix)
end
def token_set?(instance) def token_set?(instance)
raw_token = instance.read_attribute(encrypted_field) token = get_encrypted_token(instance)
unless required? unless required?
raw_token ||= insecure_strategy.get_token(instance) token ||= insecure_strategy.get_token(instance)
end end
raw_token.present? token.present? && matches_prefix?(instance, token)
end end
def encrypted_field def encrypted_field

View file

@ -18,6 +18,13 @@ class Group < Namespace
include EachBatch include EachBatch
include BulkMemberAccessLoad 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 def self.sti_name
'Group' 'Group'
end end
@ -108,7 +115,9 @@ class Group < Namespace
message: Gitlab::Regex.group_name_regex_message }, message: Gitlab::Regex.group_name_regex_message },
if: :name_changed? 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_create :post_create_hook
after_destroy :post_destroy_hook after_destroy :post_destroy_hook
@ -652,6 +661,15 @@ class Group < Namespace
ensure_runners_token! ensure_runners_token!
end 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 def project_creation_level
super || ::Gitlab::CurrentSettings.default_project_creation super || ::Gitlab::CurrentSettings.default_project_creation
end end

View file

@ -46,7 +46,7 @@ class Note < ApplicationRecord
attr_accessor :user_visible_reference_count attr_accessor :user_visible_reference_count
# Attribute used to store the attributes that have been changed by quick actions. # 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. # Attribute used to determine whether keep_around_commits will be skipped for diff notes.
attr_accessor :skip_keep_around_commits attr_accessor :skip_keep_around_commits
@ -612,6 +612,41 @@ class Note < ApplicationRecord
change_position.line_range["end"] || change_position.line_range["start"] change_position.line_range["end"] || change_position.line_range["start"]
end 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 private
def system_note_viewable_by?(user) def system_note_viewable_by?(user)

View file

@ -73,6 +73,11 @@ class Project < ApplicationRecord
GL_REPOSITORY_TYPES = [Gitlab::GlRepository::PROJECT, Gitlab::GlRepository::WIKI, Gitlab::GlRepository::DESIGN].freeze 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 cache_markdown_field :description, pipeline: :description
default_value_for :packages_enabled, true default_value_for :packages_enabled, true
@ -93,7 +98,9 @@ class Project < ApplicationRecord
default_value_for :autoclose_referenced_issues, true default_value_for :autoclose_referenced_issues, true
default_value_for(:ci_config_path) { Gitlab::CurrentSettings.default_ci_config_path } 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? } before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
@ -1843,6 +1850,15 @@ class Project < ApplicationRecord
ensure_runners_token! ensure_runners_token!
end 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? def pages_deployed?
pages_metadatum&.deployed? pages_metadatum&.deployed?
end end

View file

@ -237,15 +237,19 @@ class Snippet < ApplicationRecord
end end
end end
def all_files
list_files(default_branch)
end
def blob def blob
@blob ||= Blob.decorate(SnippetBlob.new(self), self) @blob ||= Blob.decorate(SnippetBlob.new(self), self)
end end
def blobs def blobs(paths = [])
return [] unless repository_exists? return [] unless repository_exists?
files = list_files(default_branch) paths = all_files if paths.empty?
items = files.map { |file| [default_branch, file] } items = paths.map { |path| [default_branch, path] }
repository.blobs_at(items).compact repository.blobs_at(items).compact
end end

View file

@ -19,6 +19,8 @@ module Members
end end
def execute def execute
raise Gitlab::Access::AccessDeniedError unless can?(current_user, create_member_permission(source), source)
validate_invite_source! validate_invite_source!
validate_invitable! validate_invitable!
@ -144,6 +146,17 @@ module Members
def formatted_errors def formatted_errors
errors.to_sentence errors.to_sentence
end 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
end end

View file

@ -13,4 +13,4 @@
.form-group .form-group
.well-password-auth.collapse.js-well-password-auth .well-password-auth.collapse.js-well-password-auth
= f.label :password, _("Password"), class: "label-bold" = 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'

View file

@ -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

View file

@ -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

View file

@ -8,6 +8,7 @@ end
ActionMailer::Base.register_interceptors( ActionMailer::Base.register_interceptors(
::Gitlab::Email::Hook::AdditionalHeadersInterceptor, ::Gitlab::Email::Hook::AdditionalHeadersInterceptor,
::Gitlab::Email::Hook::EmailTemplateInterceptor, ::Gitlab::Email::Hook::EmailTemplateInterceptor,
::Gitlab::Email::Hook::ValidateAddressesInterceptor,
::Gitlab::Email::Hook::DeliveryMetricsObserver ::Gitlab::Email::Hook::DeliveryMetricsObserver
) )

View file

@ -7519,6 +7519,7 @@ The connection type for [`SnippetBlob`](#snippetblob).
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="snippetblobconnectionedges"></a>`edges` | [`[SnippetBlobEdge]`](#snippetblobedge) | A list of edges. | | <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="snippetblobconnectionnodes"></a>`nodes` | [`[SnippetBlob]`](#snippetblob) | A list of nodes. |
| <a id="snippetblobconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | | <a id="snippetblobconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |

View file

@ -105,9 +105,6 @@ module API
params.except!(:created_after, :created_before, :order_by, :sort, :two_factor, :without_projects) params.except!(:created_after, :created_before, :order_by, :sort, :two_factor, :without_projects)
end end
users = UsersFinder.new(current_user, params).execute
users = reorder_users(users)
authorized = can?(current_user, :read_users_list) authorized = can?(current_user, :read_users_list)
# When `current_user` is not present, require that the `username` # 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 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 entity = current_user&.admin? ? Entities::UserWithAdmin : Entities::UserBasic
users = users.preload(:identities, :u2f_registrations) if entity == Entities::UserWithAdmin users = users.preload(:identities, :u2f_registrations) if entity == Entities::UserWithAdmin
users = users.preload(:identities, :webauthn_registrations) if entity == Entities::UserWithAdmin users = users.preload(:identities, :webauthn_registrations) if entity == Entities::UserWithAdmin

View file

@ -25,7 +25,14 @@ module Banzai
DOLLAR_SIGN = '$' 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 def call
nodes_count = 0
doc.xpath(XPATH_CODE).each do |code| doc.xpath(XPATH_CODE).each do |code|
closing = code.next closing = code.next
opening = code.previous opening = code.previous
@ -41,6 +48,9 @@ module Banzai
code[STYLE_ATTRIBUTE] = 'inline' code[STYLE_ATTRIBUTE] = 'inline'
closing.content = closing.content[1..] closing.content = closing.content[1..]
opening.content = opening.content[0..-2] opening.content = opening.content[0..-2]
nodes_count += 1
break if nodes_count >= RENDER_NODES_LIMIT
end end
end end

View 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

View file

@ -39328,6 +39328,9 @@ msgstr ""
msgid "WARNING:" msgid "WARNING:"
msgstr "" 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" msgid "Wait for the file to load to copy its contents"
msgstr "" msgstr ""

View file

@ -1,4 +1,4 @@
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { Blob, BinaryBlob } from 'jest/blob/components/mock_data'; import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
import EmbedDropdown from '~/snippets/components/embed_dropdown.vue'; 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', () => { describe('Clone button rendering', () => {
it.each` it.each`
httpUrlToRepo | sshUrlToRepo | shouldRender | isRendered httpUrlToRepo | sshUrlToRepo | shouldRender | isRendered

View file

@ -13,11 +13,14 @@ RSpec.describe Resolvers::Snippets::BlobsResolver do
let_it_be(:current_user) { create(:user) } let_it_be(:current_user) { create(:user) }
let_it_be(:snippet) { create(:personal_snippet, :private, :repository, author: current_user) } let_it_be(:snippet) { create(:personal_snippet, :private, :repository, author: current_user) }
let(:query_context) { {} }
context 'when user is not authorized' do context 'when user is not authorized' do
let(:other_user) { create(:user) } let(:other_user) { create(:user) }
it 'redacts the field' do it 'redacts the field' do
expect(resolve_blobs(snippet, user: other_user)).to be_nil expect(resolve_blobs(snippet, user: other_user)).to be_nil
expect(query_context[:unretrievable_blobs?]).to eq(false)
end end
end end
@ -28,6 +31,7 @@ RSpec.describe Resolvers::Snippets::BlobsResolver do
expect(result).to match_array(snippet.list_files.map do |file| expect(result).to match_array(snippet.list_files.map do |file|
have_attributes(path: file) have_attributes(path: file)
end) end)
expect(query_context[:unretrievable_blobs?]).to eq(false)
end end
end end
@ -37,12 +41,14 @@ RSpec.describe Resolvers::Snippets::BlobsResolver do
path = 'CHANGELOG' path = 'CHANGELOG'
expect(resolve_blobs(snippet, paths: [path])).to contain_exactly(have_attributes(path: path)) expect(resolve_blobs(snippet, paths: [path])).to contain_exactly(have_attributes(path: path))
expect(query_context[:unretrievable_blobs?]).to eq(false)
end end
end end
context 'the argument does not match anything' do context 'the argument does not match anything' do
it 'returns an empty result' do it 'returns an empty result' do
expect(resolve_blobs(snippet, paths: ['does not exist'])).to be_empty expect(resolve_blobs(snippet, paths: ['does not exist'])).to be_empty
expect(query_context[:unretrievable_blobs?]).to eq(true)
end end
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| expect(resolve_blobs(snippet, paths: paths)).to match_array(paths.map do |file|
have_attributes(path: file) have_attributes(path: file)
end) end)
expect(query_context[:unretrievable_blobs?]).to eq(false)
end end
end end
end end
end end
def resolve_blobs(snippet, user: current_user, paths: [], args: { paths: paths }) def resolve_blobs(snippet, user: current_user, paths: [], args: { paths: paths }, has_unretrievable_blobs: false)
resolve(described_class, args: args, ctx: { current_user: user }, obj: snippet) query_context[:current_user] = user
query_context[:unretrievable_blobs?] = has_unretrievable_blobs
resolve(described_class, args: args, ctx: query_context, obj: snippet)
end end
end end

View file

@ -7,6 +7,7 @@ RSpec.describe Resolvers::UsersResolver do
let_it_be(:user1) { create(:user, name: "SomePerson") } let_it_be(:user1) { create(:user, name: "SomePerson") }
let_it_be(:user2) { create(:user, username: "someone123784") } let_it_be(:user2) { create(:user, username: "someone123784") }
let_it_be(:current_user) { create(:user) }
specify do specify do
expect(described_class).to have_nullable_graphql_type(Types::UserType.connection_type) expect(described_class).to have_nullable_graphql_type(Types::UserType.connection_type)
@ -14,14 +15,14 @@ RSpec.describe Resolvers::UsersResolver do
describe '#resolve' do describe '#resolve' do
it 'raises an error when read_users_list is not authorized' 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) expect { resolve_users }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end end
context 'when no arguments are passed' do context 'when no arguments are passed' do
it 'returns all users' 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
end end
@ -65,9 +66,21 @@ RSpec.describe Resolvers::UsersResolver do
expect(resolve_users( args: { search: "someperson" } )).to contain_exactly(user1) expect(resolve_users( args: { search: "someperson" } )).to contain_exactly(user1)
end end
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 end
def resolve_users(args: {}, ctx: {}) 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
end end

View file

@ -126,4 +126,12 @@ RSpec.describe Banzai::Filter::MathFilter do
expect(before.to_s).to eq '$' expect(before.to_s).to eq '$'
expect(after.to_s).to eq '$' expect(after.to_s).to eq '$'
end 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 end

View file

@ -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

View file

@ -290,3 +290,106 @@ RSpec.describe Ci::Build, 'TokenAuthenticatable' do
end end
end 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

View file

@ -30,6 +30,21 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
expect(subject.find_token_authenticatable('my-value')) expect(subject.find_token_authenticatable('my-value'))
.to eq 'encrypted resource' .to eq 'encrypted resource'
end 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 end
context 'when encryption is optional' do context 'when encryption is optional' do
@ -56,6 +71,21 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
expect(subject.find_token_authenticatable('my-value')) expect(subject.find_token_authenticatable('my-value'))
.to eq 'plaintext resource' .to eq 'plaintext resource'
end 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 end
context 'when encryption is migrating' do context 'when encryption is migrating' do
@ -78,6 +108,21 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
expect(subject.find_token_authenticatable('my-value')) expect(subject.find_token_authenticatable('my-value'))
.to be_nil .to be_nil
end 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
end end

View file

@ -2775,4 +2775,12 @@ RSpec.describe Group do
end end
end 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 end

View file

@ -1624,4 +1624,14 @@ RSpec.describe Note do
end end
end 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 end

View file

@ -765,8 +765,8 @@ RSpec.describe Project, factory_default: :keep do
end end
it 'does not set an random token if one provided' do it 'does not set an random token if one provided' do
project = FactoryBot.create(:project, runners_token: 'my-token') project = FactoryBot.create(:project, runners_token: "#{Project::RUNNERS_TOKEN_PREFIX}my-token")
expect(project.runners_token).to eq('my-token') expect(project.runners_token).to eq("#{Project::RUNNERS_TOKEN_PREFIX}my-token")
end end
end end
@ -7551,6 +7551,14 @@ RSpec.describe Project, factory_default: :keep do
end end
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 private
def finish_job(export_job) def finish_job(export_job)

View file

@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Snippet do RSpec.describe Snippet do
include FakeBlobHelpers
describe 'modules' do describe 'modules' do
subject { described_class } subject { described_class }
@ -526,6 +528,21 @@ RSpec.describe Snippet do
end end
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 describe '#blobs' do
context 'when repository does not exist' do context 'when repository does not exist' do
let(:snippet) { create(:snippet) } let(:snippet) { create(:snippet) }
@ -552,6 +569,23 @@ RSpec.describe Snippet do
end end
end 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 end
describe '#to_json' do describe '#to_json' do

View file

@ -5,11 +5,13 @@ require 'spec_helper'
RSpec.describe 'Users' do RSpec.describe 'Users' do
include GraphqlHelpers 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(:user1) { create(:user, created_at: 2.days.ago) }
let_it_be(:user2) { create(:user, created_at: 3.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_it_be(:user3) { create(:user, created_at: 4.days.ago) }
let(:current_user) { user0 }
describe '.users' do describe '.users' do
shared_examples 'a working users query' do shared_examples 'a working users query' do
it_behaves_like 'a working graphql query' do it_behaves_like 'a working graphql query' do
@ -19,7 +21,7 @@ RSpec.describe 'Users' do
end end
it 'includes a list of users' do 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 expect(graphql_data.dig('users', 'nodes')).not_to be_empty
end 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 }') } 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 it 'displays an error' do
post_graphql(query) post_graphql(query, current_user: current_user)
expect(graphql_errors).to include( expect(graphql_errors).to include(
a_hash_including('message' => a_string_matching(%r{Provide either a list of usernames or ids})) 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_behaves_like 'a working users query'
it 'includes all non-admin users', :aggregate_failures do it 'includes all users', :aggregate_failures do
post_graphql(query) post_query
expect(graphql_data.dig('users', 'nodes')).to include( expect(graphql_data.dig('users', 'nodes')).to include(
{ "id" => user0.to_global_id.to_s },
{ "id" => user1.to_global_id.to_s }, { "id" => user1.to_global_id.to_s },
{ "id" => user2.to_global_id.to_s }, { "id" => user2.to_global_id.to_s },
{ "id" => user3.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" => admin.to_global_id.to_s },
{ "id" => another_admin.to_global_id.to_s } { "id" => another_admin.to_global_id.to_s }
) )
@ -81,10 +83,12 @@ RSpec.describe 'Users' do
end end
context 'when current user is an admin' do context 'when current user is an admin' do
let(:current_user) { admin }
it_behaves_like 'a working users query' it_behaves_like 'a working users query'
it 'includes only admins', :aggregate_failures do 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( expect(graphql_data.dig('users', 'nodes')).to include(
{ "id" => another_admin.to_global_id.to_s }, { "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( expect(graphql_data.dig('users', 'nodes')).not_to include(
{ "id" => user0.to_global_id.to_s },
{ "id" => user1.to_global_id.to_s }, { "id" => user1.to_global_id.to_s },
{ "id" => user2.to_global_id.to_s }, { "id" => user2.to_global_id.to_s },
{ "id" => user3.to_global_id.to_s }, { "id" => user3.to_global_id.to_s }
{ "id" => current_user.to_global_id.to_s }
) )
end end
end end
@ -110,7 +114,7 @@ RSpec.describe 'Users' do
end end
context 'when sorting by created_at' do 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 context 'when ascending' do
it_behaves_like 'sorted paginated query' do it_behaves_like 'sorted paginated query' do

View file

@ -233,11 +233,9 @@ RSpec.describe API::Notes do
subject { post api(request_path, user), params: { body: request_body } } subject { post api(request_path, user), params: { body: request_body } }
context 'a command only note' do context 'a command only note' do
let(:assignee) { create(:user) } let(:request_body) { "/spend 1h" }
let(:request_body) { "/assign #{assignee.to_reference}" }
before do before do
project.add_developer(assignee)
project.add_developer(user) project.add_developer(user)
end end
@ -256,7 +254,7 @@ RSpec.describe API::Notes do
end end
it 'applies the commands' do it 'applies the commands' do
expect { subject }.to change { merge_request.reset.assignees } expect { subject }.to change { merge_request.reset.total_time_spent }
end end
it 'reports the changes' do it 'reports the changes' do
@ -264,9 +262,9 @@ RSpec.describe API::Notes do
expect(json_response).to include( expect(json_response).to include(
'commands_changes' => 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
end end

View file

@ -11,19 +11,37 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
let(:additional_params) { { invite_source: '_invite_source_' } } let(:additional_params) { { invite_source: '_invite_source_' } }
let(:params) { { user_ids: user_ids, access_level: access_level }.merge(additional_params) } 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 before do
if source.is_a?(Project) case source
when Project
source.add_maintainer(user) source.add_maintainer(user)
OnboardingProgress.onboard(source.namespace) OnboardingProgress.onboard(source.namespace)
else when Group
source.add_owner(user) source.add_owner(user)
OnboardingProgress.onboard(source) OnboardingProgress.onboard(source)
end end
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 context 'when passing valid parameters' do
it 'adds a user to members' do it 'adds a user to members' do
expect(execute_service[:status]).to eq(:success) expect(execute_service[:status]).to eq(:success)

View file

@ -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