Commit 57c22b27 authored by GitLab Release Tools Bot's avatar GitLab Release Tools Bot

Merge remote-tracking branch 'dev/master'

parents 982b3ee7 df452c21
...@@ -2,6 +2,19 @@ ...@@ -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.8.2 (2022-02-25)
### Security (8 changes)
- [Limit commands_changes to certain keys](gitlab-org/security/gitlab@7a4e348b3ea3d34469bcd353286474c25288d836) ([merge request](gitlab-org/security/gitlab!2225))
- [Add runners_token prefix to Group and Project](gitlab-org/security/gitlab@87bd94f7252f887f22f971ffd59044b355712042) ([merge request](gitlab-org/security/gitlab!2248))
- [Anonymous user can enumerate all users through GraphQL endpoint](gitlab-org/security/gitlab@945da4fadb156ce862bdd12ee5625f57709b590d) ([merge request](gitlab-org/security/gitlab!2213))
- [Check for unsafe characters in email addresses before sending](gitlab-org/security/gitlab@641b23f6b1ad827536ea704c848330a068fc0472) ([merge request](gitlab-org/security/gitlab!2206))
- [Warn when snippet contains unretrievable files](gitlab-org/security/gitlab@d703ecef74f7d73eab3d5345af3a5d60f28c9d7f) ([merge request](gitlab-org/security/gitlab!2205))
- [Prevent DOS when rendering math markdown](gitlab-org/security/gitlab@cedf63be73dc9c37352e425cf8b8cf4e16980935) ([merge request](gitlab-org/security/gitlab!2219))
- [Check permission when creating members through service](gitlab-org/security/gitlab@6228fd285e7062dd1c2e88f8ca33bc8e9a0f4fad) ([merge request](gitlab-org/security/gitlab!2209))
- [Reset password field on page load](gitlab-org/security/gitlab@5ece8645add37c3e77f28d7afb8f28ce4bbe2b7f) ([merge request](gitlab-org/security/gitlab!2224))
## 14.8.1 (2022-02-23) ## 14.8.1 (2022-02-23)
### Fixed (3 changes) ### Fixed (3 changes)
...@@ -678,6 +691,19 @@ entry. ...@@ -678,6 +691,19 @@ entry.
- [Use `ssh_data` gem instead of `net-ssh` and `sshkey` where possible](gitlab-org/gitlab@59a0ee8605d509753c9aec719f8e0da77bcc679d) ([merge request](gitlab-org/gitlab!77424)) - [Use `ssh_data` gem instead of `net-ssh` and `sshkey` where possible](gitlab-org/gitlab@59a0ee8605d509753c9aec719f8e0da77bcc679d) ([merge request](gitlab-org/gitlab!77424))
- [Remove feature flag already default enabled](gitlab-org/gitlab@9b7059a4bf9dc2ecdce1910a931cc6967d05b5ad) ([merge request](gitlab-org/gitlab!78238)) **GitLab Enterprise Edition** - [Remove feature flag already default enabled](gitlab-org/gitlab@9b7059a4bf9dc2ecdce1910a931cc6967d05b5ad) ([merge request](gitlab-org/gitlab!78238)) **GitLab Enterprise Edition**
## 14.7.4 (2022-02-25)
### Security (8 changes)
- [Limit commands_changes to certain keys](gitlab-org/security/gitlab@59351be8d1d868e31bc849482b266e4047710eeb) ([merge request](gitlab-org/security/gitlab!2226))
- [Add runners_token prefix to Group and Project](gitlab-org/security/gitlab@74615b7fd5359c4da7f1a3ca9052685e81e1690f) ([merge request](gitlab-org/security/gitlab!2249))
- [Anonymous user can enumerate all users through GraphQL endpoint](gitlab-org/security/gitlab@e213dfc546020d3f88b40cdfc0f877138b0aaef5) ([merge request](gitlab-org/security/gitlab!2119))
- [Check for unsafe characters in email addresses before sending](gitlab-org/security/gitlab@395385ffccfd9d25063531ea955b179d9bc4f0c5) ([merge request](gitlab-org/security/gitlab!2207))
- [Warn when snippet contains unretrievable files](gitlab-org/security/gitlab@dc4b3c00284e17bcbf20ec2ae1ee7e8a7efae9b2) ([merge request](gitlab-org/security/gitlab!2204))
- [Prevent DOS when rendering math markdown](gitlab-org/security/gitlab@f01674f210dee4c803b4850292d16412463b18e3) ([merge request](gitlab-org/security/gitlab!2200))
- [Check permission when creating members through service](gitlab-org/security/gitlab@4f9b302511ddfaf07af8d08d848252e0c64ff307) ([merge request](gitlab-org/security/gitlab!2210))
- [Reset password field on page load](gitlab-org/security/gitlab@1a6541462e1ddd58ea9a172fbd3c0b9026760784) ([merge request](gitlab-org/security/gitlab!2193))
## 14.7.3 (2022-02-15) ## 14.7.3 (2022-02-15)
### Fixed (2 changes) ### Fixed (2 changes)
...@@ -1153,6 +1179,19 @@ See https://about.gitlab.com/releases/2022/02/03/security-release-gitlab-14-7-1- ...@@ -1153,6 +1179,19 @@ See https://about.gitlab.com/releases/2022/02/03/security-release-gitlab-14-7-1-
- [Fix Gitlab/DelegatePredicateMethods offenses](gitlab-org/gitlab@518700a11025b0000ff3ce011638417a882612b0) by @edith007 ([merge request](gitlab-org/gitlab!76001)) - [Fix Gitlab/DelegatePredicateMethods offenses](gitlab-org/gitlab@518700a11025b0000ff3ce011638417a882612b0) by @edith007 ([merge request](gitlab-org/gitlab!76001))
- [Fix Rails/SaveBang offenses](gitlab-org/gitlab@513b0e1dbdf95ea595e7548ff26929e0be30ce29) by @edith007 ([merge request](gitlab-org/gitlab!75894)) **GitLab Enterprise Edition** - [Fix Rails/SaveBang offenses](gitlab-org/gitlab@513b0e1dbdf95ea595e7548ff26929e0be30ce29) by @edith007 ([merge request](gitlab-org/gitlab!75894)) **GitLab Enterprise Edition**
## 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)
### Security ### Security
...@@ -6,6 +6,8 @@ import { __ } from '~/locale'; ...@@ -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 { ...@@ -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 { ...@@ -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() {
......
<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 { ...@@ -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 { ...@@ -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 { ...@@ -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"
......
...@@ -17,6 +17,7 @@ export const getSnippetMixin = { ...@@ -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;
......
...@@ -15,6 +15,7 @@ query GetSnippetQuery($ids: [SnippetID!]) { ...@@ -15,6 +15,7 @@ query GetSnippetQuery($ids: [SnippetID!]) {
sshUrlToRepo sshUrlToRepo
blobs { blobs {
__typename __typename
hasUnretrievableBlobs
nodes { nodes {
__typename __typename
binary binary
......
...@@ -12,6 +12,7 @@ query SnippetBlobContent($ids: [ID!], $rich: Boolean!, $paths: [String!]) { ...@@ -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
} }
} }
} }
......
...@@ -19,18 +19,18 @@ module Resolvers ...@@ -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)) # TODO: Some blobs, e.g. those with non-utf8 filenames, are returned as nil from the
end # repository. We need to provide a flag to notify the user of this until we come up with a
end # 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
private # of https://gitlab.com/gitlab-org/gitlab/-/issues/323082, at which point this attribute may
# be removed.
def transformed_blob_paths(paths) context[:unretrievable_blobs?] = blobs.size < paths.size
ref = snippet.default_branch
paths.map { |path| [ref, path] } blobs
end end
end end
end end
......
...@@ -29,7 +29,7 @@ module Resolvers ...@@ -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 ...@@ -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
......
# 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 ...@@ -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
......
...@@ -5,16 +5,18 @@ module TokenAuthenticatableStrategies ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
......
...@@ -20,6 +20,13 @@ class Group < Namespace ...@@ -20,6 +20,13 @@ class Group < Namespace
include ChronicDurationAttribute include ChronicDurationAttribute
include RunnerTokenExpirationInterval include RunnerTokenExpirationInterval
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
...@@ -115,7 +122,9 @@ class Group < Namespace ...@@ -115,7 +122,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
...@@ -669,6 +678,15 @@ class Group < Namespace ...@@ -669,6 +678,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
......
...@@ -50,7 +50,7 @@ class Note < ApplicationRecord ...@@ -50,7 +50,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
...@@ -615,6 +615,41 @@ class Note < ApplicationRecord ...@@ -615,6 +615,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)
......
...@@ -89,6 +89,11 @@ class Project < ApplicationRecord ...@@ -89,6 +89,11 @@ class Project < ApplicationRecord
DEFAULT_SQUASH_COMMIT_TEMPLATE = '%{title}' DEFAULT_SQUASH_COMMIT_TEMPLATE = '%{title}'
# 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
...@@ -109,7 +114,9 @@ class Project < ApplicationRecord ...@@ -109,7 +114,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? }
...@@ -1870,6 +1877,15 @@ class Project < ApplicationRecord ...@@ -1870,6 +1877,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
......
...@@ -237,15 +237,19 @@ class Snippet < ApplicationRecord ...@@ -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
......
...@@ -19,6 +19,8 @@ module Members ...@@ -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!
...@@ -156,6 +158,17 @@ module Members ...@@ -156,6 +158,17 @@ module Members
}) })
) )
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
......
...@@ -13,4 +13,4 @@ ...@@ -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'
---
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
---
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 ...@@ -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
) )
......
...@@ -7921,6 +7921,7 @@ The connection type for [`SnippetBlob`](#snippetblob). ...@@ -7921,6 +7921,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. |
...@@ -77,4 +77,43 @@ RSpec.describe API::Invitations, 'EE Invitations' do ...@@ -77,4 +77,43 @@ RSpec.describe API::Invitations, 'EE Invitations' do
end end
end end
end end
context 'group with LDAP group link' do
include LdapHelpers
let(:group) { create(:group_with_ldap_group_link, :public) }
let(:owner) { create(:user) }
let(:developer) { create(:user) }
let(:invite) { create(:group_member, :invited, source: group, user: developer) }
before do
create(:group_member, :owner, group: group, user: owner)
stub_ldap_setting(enabled: true)
stub_application_setting(lock_memberships_to_ldap: true)
end
describe 'POST /groups/:id/invitations' do
it 'returns a forbidden response' do
post api("/groups/#{group.id}/invitations", owner), params: { email: developer.email, access_level: Member::DEVELOPER }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
describe 'PUT /groups/:id/invitations/:email' do
it 'returns a forbidden response' do
put api("/groups/#{group.id}/invitations/#{invite.invite_email}", owner), params: { access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
describe 'DELETE /groups/:id/invitations/:email' do
it 'returns a forbidden response' do
delete api("/groups/#{group.id}/invitations/#{invite.invite_email}", owner)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
end end
...@@ -928,6 +928,17 @@ RSpec.describe API::Members do ...@@ -928,6 +928,17 @@ RSpec.describe API::Members do
end end
end end
describe 'POST /groups/:id/members' do
let(:stranger) { create(:user) }
it 'returns a forbidden response' do
post api("/groups/#{group.id}/members", owner),
params: { user_id: stranger.id, access_level: Member::DEVELOPER }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
describe 'POST /groups/:id/members/:user_id/override' do describe 'POST /groups/:id/members/:user_id/override' do
it 'succeeds when override is set on an LDAP user' do it 'succeeds when override is set on an LDAP user' do
post api("/groups/#{group.id}/members/#{ldap_developer.id}/override", owner) post api("/groups/#{group.id}/members/#{ldap_developer.id}/override", owner)
......
...@@ -105,9 +105,6 @@ module API ...@@ -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 ...@@ -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
......
...@@ -25,7 +25,14 @@ module Banzai ...@@ -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 ...@@ -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
......
# 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
...@@ -40897,6 +40897,9 @@ msgstr "" ...@@ -40897,6 +40897,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 ""
......
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', () => { ...@@ -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
......
...@@ -13,11 +13,14 @@ RSpec.describe Resolvers::Snippets::BlobsResolver do ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
...@@ -7,6 +7,7 @@ RSpec.describe Resolvers::UsersResolver do ...@@ -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 ...@@ -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 ...@@ -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
...@@ -126,4 +126,12 @@ RSpec.describe Banzai::Filter::MathFilter do ...@@ -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
# 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
...@@ -428,3 +428,106 @@ RSpec.describe Ci::Runner, 'TokenAuthenticatable', :freeze_time do ...@@ -428,3 +428,106 @@ RSpec.describe Ci::Runner, 'TokenAuthenticatable', :freeze_time 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
...@@ -32,6 +32,21 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do ...@@ -32,6 +32,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
...@@ -62,6 +77,21 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do ...@@ -62,6 +77,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
...@@ -88,6 +118,21 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do ...@@ -88,6 +118,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
......
...@@ -3190,4 +3190,12 @@ RSpec.describe Group do ...@@ -3190,4 +3190,12 @@ RSpec.describe Group do
it_behaves_like 'no effective expiration interval' it_behaves_like 'no effective expiration interval'
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
...@@ -1645,4 +1645,14 @@ RSpec.describe Note do ...@@ -1645,4 +1645,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
...@@ -782,8 +782,8 @@ RSpec.describe Project, factory_default: :keep do ...@@ -782,8 +782,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
...@@ -8032,6 +8032,14 @@ RSpec.describe Project, factory_default: :keep do ...@@ -8032,6 +8032,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)
......
...@@ -3,6 +3,8 @@ ...@@ -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 ...@@ -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 ...@@ -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
......
...@@ -5,11 +5,13 @@ require 'spec_helper' ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
......
...@@ -233,11 +233,9 @@ RSpec.describe API::Notes do ...@@ -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 ...@@ -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 ...@@ -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
......
...@@ -11,19 +11,37 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ ...@@ -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)
......
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment