Update upstream source from tag 'upstream/13.6.7'

Update to upstream version '13.6.7'
with Debian dir 98b8813be2
This commit is contained in:
Pirate Praveen 2021-02-11 23:38:06 +05:30
commit 6a0a4e2d68
50 changed files with 720 additions and 123 deletions

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.
## 13.6.7 (2021-02-11)
### Security (7 changes)
- Cancel running and pending jobs when a project is deleted. !1220
- Updates authorization for linting API.
- Prevent exposure of confidential issue titles in file browser.
- Check user access on API merge request read actions.
- Prevent Denial of Service Attack on gitlab-shell.
- Limit daily invitations to groups and projects.
- Prevent Server-side Request Forgery for Prometheus when secured by Google IAP.
## 13.6.6 (2021-02-01) ## 13.6.6 (2021-02-01)
### Security (5 changes) ### Security (5 changes)

View file

@ -1 +1 @@
13.6.6 13.6.7

View file

@ -1 +1 @@
13.13.0 13.13.1

View file

@ -1 +1 @@
13.6.6 13.6.7

View file

@ -15,6 +15,7 @@ module Ci
include ShaAttribute include ShaAttribute
include FromUnion include FromUnion
include UpdatedAtFilterable include UpdatedAtFilterable
include EachBatch
PROJECT_ROUTE_AND_NAMESPACE_ROUTE = { PROJECT_ROUTE_AND_NAMESPACE_ROUTE = {
project: [:project_feature, :route, { namespace: :route }] project: [:project_feature, :route, { namespace: :route }]

View file

@ -55,6 +55,7 @@ class CommitStatus < ApplicationRecord
scope :for_ids, -> (ids) { where(id: ids) } scope :for_ids, -> (ids) { where(id: ids) }
scope :for_ref, -> (ref) { where(ref: ref) } scope :for_ref, -> (ref) { where(ref: ref) }
scope :by_name, -> (name) { where(name: name) } scope :by_name, -> (name) { where(name: name) }
scope :in_pipelines, ->(pipelines) { where(pipeline: pipelines) }
scope :for_project_paths, -> (paths) do scope :for_project_paths, -> (paths) do
where(project: Project.where_full_path_in(Array(paths))) where(project: Project.where_full_path_in(Array(paths)))

View file

@ -45,6 +45,19 @@ class Member < ApplicationRecord
}, },
if: :project_bot? if: :project_bot?
scope :in_hierarchy, ->(source) do
groups = source.root_ancestor.self_and_descendants
group_members = Member.default_scoped.where(source: groups)
projects = source.root_ancestor.all_projects
project_members = Member.default_scoped.where(source: projects)
Member.default_scoped.from_union([
group_members,
project_members
]).merge(self)
end
# This scope encapsulates (most of) the conditions a row in the member table # This scope encapsulates (most of) the conditions a row in the member table
# must satisfy if it is a valid permission. Of particular note: # must satisfy if it is a valid permission. Of particular note:
# #
@ -77,12 +90,18 @@ class Member < ApplicationRecord
scope :invite, -> { where.not(invite_token: nil) } scope :invite, -> { where.not(invite_token: nil) }
scope :non_invite, -> { where(invite_token: nil) } scope :non_invite, -> { where(invite_token: nil) }
scope :request, -> { where.not(requested_at: nil) } scope :request, -> { where.not(requested_at: nil) }
scope :non_request, -> { where(requested_at: nil) } scope :non_request, -> { where(requested_at: nil) }
scope :not_accepted_invitations, -> { invite.where(invite_accepted_at: nil) } scope :not_accepted_invitations, -> { invite.where(invite_accepted_at: nil) }
scope :not_accepted_invitations_by_user, -> (user) { not_accepted_invitations.where(created_by: user) } scope :not_accepted_invitations_by_user, -> (user) { not_accepted_invitations.where(created_by: user) }
scope :not_expired, -> (today = Date.current) { where(arel_table[:expires_at].gt(today).or(arel_table[:expires_at].eq(nil))) } scope :not_expired, -> (today = Date.current) { where(arel_table[:expires_at].gt(today).or(arel_table[:expires_at].eq(nil))) }
scope :created_today, -> do
now = Date.current
where(created_at: now.beginning_of_day..now.end_of_day)
end
scope :last_ten_days_excluding_today, -> (today = Date.current) { where(created_at: (today - 10).beginning_of_day..(today - 1).end_of_day) } scope :last_ten_days_excluding_today, -> (today = Date.current) { where(created_at: (today - 10).beginning_of_day..(today - 1).end_of_day) }
scope :has_access, -> { active.where('access_level > 0') } scope :has_access, -> { active.where('access_level > 0') }

View file

@ -183,7 +183,17 @@ class PrometheusService < MonitoringService
manual_configuration? && google_iap_audience_client_id.present? && google_iap_service_account_json.present? manual_configuration? && google_iap_audience_client_id.present? && google_iap_service_account_json.present?
end end
def clean_google_iap_service_account
return unless google_iap_service_account_json
google_iap_service_account_json
.then { |json| Gitlab::Json.parse(json) }
.except('token_credential_uri')
end
def iap_client def iap_client
@iap_client ||= Google::Auth::Credentials.new(Gitlab::Json.parse(google_iap_service_account_json), target_audience: google_iap_audience_client_id).client @iap_client ||= Google::Auth::Credentials
.new(clean_google_iap_service_account, target_audience: google_iap_audience_client_id)
.client
end end
end end

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Ci
class AbortProjectPipelinesService
# Danger: Cancels in bulk without callbacks
# Only for pipeline abandonment scenarios (current example: project delete)
def execute(project)
return unless Feature.enabled?(:abort_deleted_project_pipelines, default_enabled: true)
pipelines = project.all_pipelines.cancelable
bulk_abort!(pipelines, status: :canceled)
ServiceResponse.success(message: 'Pipelines canceled')
end
private
def bulk_abort!(pipelines, status:)
pipelines.each_batch do |pipeline_batch|
CommitStatus.in_pipelines(pipeline_batch).in_batches.update_all(status: status) # rubocop: disable Cop/InBatches
pipeline_batch.update_all(status: status)
end
end
end
end

View file

@ -6,6 +6,7 @@ module Ci
# This is a bug with CodeReuse/ActiveRecord cop # This is a bug with CodeReuse/ActiveRecord cop
# https://gitlab.com/gitlab-org/gitlab/issues/32332 # https://gitlab.com/gitlab-org/gitlab/issues/32332
def execute(user) def execute(user)
# TODO: fix N+1 queries https://gitlab.com/gitlab-org/gitlab/-/issues/300685
user.pipelines.cancelable.find_each(&:cancel_running) user.pipelines.cancelable.find_each(&:cancel_running)
ServiceResponse.success(message: 'Pipeline canceled') ServiceResponse.success(message: 'Pipeline canceled')

View file

@ -2,12 +2,12 @@
module Members module Members
class CreateService < Members::BaseService class CreateService < Members::BaseService
include Gitlab::Utils::StrongMemoize
DEFAULT_LIMIT = 100 DEFAULT_LIMIT = 100
def execute(source) def execute(source)
return error(s_('AddMember|No users specified.')) if params[:user_ids].blank? return error(s_('AddMember|No users specified.')) if user_ids.blank?
user_ids = params[:user_ids].split(',').uniq.flatten
return error(s_("AddMember|Too many users specified (limit is %{user_limit})") % { user_limit: user_limit }) if return error(s_("AddMember|Too many users specified (limit is %{user_limit})") % { user_limit: user_limit }) if
user_limit && user_ids.size > user_limit user_limit && user_ids.size > user_limit
@ -45,6 +45,13 @@ module Members
private private
def user_ids
strong_memoize(:user_ids) do
ids = params[:user_ids] || ''
ids.split(',').uniq.flatten
end
end
def user_limit def user_limit
limit = params.fetch(:limit, DEFAULT_LIMIT) limit = params.fetch(:limit, DEFAULT_LIMIT)

View file

@ -21,11 +21,14 @@ module Projects
def execute def execute
return false unless can?(current_user, :remove_project, project) return false unless can?(current_user, :remove_project, project)
project.update_attribute(:pending_delete, true)
# Flush the cache for both repositories. This has to be done _before_ # Flush the cache for both repositories. This has to be done _before_
# removing the physical repositories as some expiration code depends on # removing the physical repositories as some expiration code depends on
# Git data (e.g. a list of branch names). # Git data (e.g. a list of branch names).
flush_caches(project) flush_caches(project)
::Ci::AbortProjectPipelinesService.new.execute(project)
Projects::UnlinkForkService.new(project, current_user).execute Projects::UnlinkForkService.new(project, current_user).execute
attempt_destroy(project) attempt_destroy(project)

View file

@ -0,0 +1,8 @@
---
name: abort_deleted_project_pipelines
introduced_by_url: https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/1220
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/301106
milestone: '13.9'
type: development
group: group::continuous integration
default_enabled: true

View file

@ -1,8 +0,0 @@
---
name: codequality_mr_diff
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47938
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/284140
milestone: '13.7'
type: development
group: group::testing
default_enabled: false

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddDailyInvitesToPlanLimits < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
add_column(:plan_limits, :daily_invites, :integer, default: 0, null: false)
end
end

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
class InsertDailyInvitesPlanLimits < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
return unless Gitlab.com?
create_or_update_plan_limit('daily_invites', 'free', 20)
create_or_update_plan_limit('daily_invites', 'bronze', 0)
create_or_update_plan_limit('daily_invites', 'silver', 0)
create_or_update_plan_limit('daily_invites', 'gold', 0)
end
def down
return unless Gitlab.com?
create_or_update_plan_limit('daily_invites', 'free', 0)
create_or_update_plan_limit('daily_invites', 'bronze', 0)
create_or_update_plan_limit('daily_invites', 'silver', 0)
create_or_update_plan_limit('daily_invites', 'gold', 0)
end
end

View file

@ -0,0 +1 @@
1200747265d5095a86250020786d6f1e9e50bc75328a71de497046807afa89d7

View file

@ -0,0 +1 @@
febefead6f966960f6493d29add5f35fc4a1080b5118c5526502fa5fe1d29023

View file

@ -14732,7 +14732,8 @@ CREATE TABLE plan_limits (
golang_max_file_size bigint DEFAULT 104857600 NOT NULL, golang_max_file_size bigint DEFAULT 104857600 NOT NULL,
debian_max_file_size bigint DEFAULT '3221225472'::bigint NOT NULL, debian_max_file_size bigint DEFAULT '3221225472'::bigint NOT NULL,
project_feature_flags integer DEFAULT 200 NOT NULL, project_feature_flags integer DEFAULT 200 NOT NULL,
ci_max_artifact_size_api_fuzzing integer DEFAULT 0 NOT NULL ci_max_artifact_size_api_fuzzing integer DEFAULT 0 NOT NULL,
daily_invites integer DEFAULT 0 NOT NULL
); );
CREATE SEQUENCE plan_limits_id_seq CREATE SEQUENCE plan_limits_id_seq

View file

@ -96,6 +96,13 @@ Read more on the [Rack Attack initializer](../security/rack_attack.md) method of
- **Default rate limit** - Disabled - **Default rate limit** - Disabled
### Member Invitations
Limit the maximum daily member invitations allowed per group hierarchy.
- GitLab.com: Free members may invite 20 members per day.
- Self-managed: Invites are not limited.
## Gitaly concurrency limit ## Gitaly concurrency limit
Clone traffic can put a large strain on your Gitaly service. To prevent such workloads from overwhelming your Gitaly server, you can set concurrency limits in Gitalys configuration file. Clone traffic can put a large strain on your Gitaly service. To prevent such workloads from overwhelming your Gitaly server, you can set concurrency limits in Gitalys configuration file.

View file

@ -182,6 +182,8 @@ service account can be found at Google's documentation for
Prometheus OAuth Client secured with Google IAP. Prometheus OAuth Client secured with Google IAP.
1. (Optional) In **Google IAP Service Account JSON**, provide the contents of the 1. (Optional) In **Google IAP Service Account JSON**, provide the contents of the
Service Account credentials file that is authorized to access the Prometheus resource. Service Account credentials file that is authorized to access the Prometheus resource.
The JSON key `token_credential_uri` is discarded to prevent
[Server-side Request Forgery (SSRF)](https://www.hackerone.com/blog-How-To-Server-Side-Request-Forgery-SSRF).
1. Click **Save changes**. 1. Click **Save changes**.
![Configure Prometheus Service](img/prometheus_manual_configuration_v13_2.png) ![Configure Prometheus Service](img/prometheus_manual_configuration_v13_2.png)

View file

@ -11,6 +11,8 @@ module API
optional :include_merged_yaml, type: Boolean, desc: 'Whether or not to include merged CI config yaml in the response' optional :include_merged_yaml, type: Boolean, desc: 'Whether or not to include merged CI config yaml in the response'
end end
post '/lint' do post '/lint' do
unauthorized! unless Gitlab::CurrentSettings.signup_enabled? && current_user
result = Gitlab::Ci::YamlProcessor.new(params[:content], user: current_user).execute result = Gitlab::Ci::YamlProcessor.new(params[:content], user: current_user).execute
error = result.errors.first error = result.errors.first
@ -56,7 +58,7 @@ module API
optional :dry_run, type: Boolean, default: false, desc: 'Run pipeline creation simulation, or only do static check.' optional :dry_run, type: Boolean, default: false, desc: 'Run pipeline creation simulation, or only do static check.'
end end
post ':id/ci/lint' do post ':id/ci/lint' do
authorize! :download_code, user_project authorize! :create_pipeline, user_project
result = Gitlab::Ci::Lint result = Gitlab::Ci::Lint
.new(project: user_project, current_user: current_user) .new(project: user_project, current_user: current_user)

View file

@ -26,6 +26,8 @@ module API
# GET /projects/:id/merge_requests/:merge_request_iid/approvals # GET /projects/:id/merge_requests/:merge_request_iid/approvals
desc 'List approvals for merge request' desc 'List approvals for merge request'
get 'approvals' do get 'approvals' do
not_found!("Merge Request") unless can?(current_user, :read_merge_request, user_project)
merge_request = find_merge_request_with_access(params[:merge_request_iid]) merge_request = find_merge_request_with_access(params[:merge_request_iid])
present_approval(merge_request) present_approval(merge_request)

View file

@ -23,6 +23,8 @@ module API
use :pagination use :pagination
end end
get ":id/merge_requests/:merge_request_iid/versions" do get ":id/merge_requests/:merge_request_iid/versions" do
not_found!("Merge Request") unless can?(current_user, :read_merge_request, user_project)
merge_request = find_merge_request_with_access(params[:merge_request_iid]) merge_request = find_merge_request_with_access(params[:merge_request_iid])
present paginate(merge_request.merge_request_diffs.order_id_desc), with: Entities::MergeRequestDiff present paginate(merge_request.merge_request_diffs.order_id_desc), with: Entities::MergeRequestDiff
@ -39,6 +41,8 @@ module API
end end
get ":id/merge_requests/:merge_request_iid/versions/:version_id" do get ":id/merge_requests/:merge_request_iid/versions/:version_id" do
not_found!("Merge Request") unless can?(current_user, :read_merge_request, user_project)
merge_request = find_merge_request_with_access(params[:merge_request_iid]) merge_request = find_merge_request_with_access(params[:merge_request_iid])
present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull

View file

@ -243,6 +243,8 @@ module API
success Entities::MergeRequest success Entities::MergeRequest
end end
get ':id/merge_requests/:merge_request_iid' do get ':id/merge_requests/:merge_request_iid' do
not_found!("Merge Request") unless can?(current_user, :read_merge_request, user_project)
merge_request = find_merge_request_with_access(params[:merge_request_iid]) merge_request = find_merge_request_with_access(params[:merge_request_iid])
present merge_request, present merge_request,
@ -259,7 +261,10 @@ module API
success Entities::UserBasic success Entities::UserBasic
end end
get ':id/merge_requests/:merge_request_iid/participants' do get ':id/merge_requests/:merge_request_iid/participants' do
not_found!("Merge Request") unless can?(current_user, :read_merge_request, user_project)
merge_request = find_merge_request_with_access(params[:merge_request_iid]) merge_request = find_merge_request_with_access(params[:merge_request_iid])
participants = ::Kaminari.paginate_array(merge_request.participants) participants = ::Kaminari.paginate_array(merge_request.participants)
present paginate(participants), with: Entities::UserBasic present paginate(participants), with: Entities::UserBasic
@ -269,6 +274,8 @@ module API
success Entities::Commit success Entities::Commit
end end
get ':id/merge_requests/:merge_request_iid/commits' do get ':id/merge_requests/:merge_request_iid/commits' do
not_found!("Merge Request") unless can?(current_user, :read_merge_request, user_project)
merge_request = find_merge_request_with_access(params[:merge_request_iid]) merge_request = find_merge_request_with_access(params[:merge_request_iid])
commits = commits =
@ -350,6 +357,8 @@ module API
success Entities::MergeRequestChanges success Entities::MergeRequestChanges
end end
get ':id/merge_requests/:merge_request_iid/changes' do get ':id/merge_requests/:merge_request_iid/changes' do
not_found!("Merge Request") unless can?(current_user, :read_merge_request, user_project)
merge_request = find_merge_request_with_access(params[:merge_request_iid]) merge_request = find_merge_request_with_access(params[:merge_request_iid])
present merge_request, present merge_request,
@ -365,6 +374,8 @@ module API
get ':id/merge_requests/:merge_request_iid/pipelines' do get ':id/merge_requests/:merge_request_iid/pipelines' do
pipelines = merge_request_pipelines_with_access pipelines = merge_request_pipelines_with_access
not_found!("Merge Request") unless can?(current_user, :read_merge_request, user_project)
present paginate(pipelines), with: Entities::Ci::PipelineBasic present paginate(pipelines), with: Entities::Ci::PipelineBasic
end end

View file

@ -28,6 +28,11 @@ module API
end end
post ":id/#{type}/:#{type_id_str}/todo" do post ":id/#{type}/:#{type_id_str}/todo" do
issuable = instance_exec(params[type_id_str], &finder) issuable = instance_exec(params[type_id_str], &finder)
unless can?(current_user, :read_merge_request, issuable.project)
not_found!(type.split("_").map(&:capitalize).join(" "))
end
todo = TodoService.new.mark_todo(issuable, current_user).first todo = TodoService.new.mark_todo(issuable, current_user).first
if todo if todo

View file

@ -10,6 +10,10 @@ module Gitlab
include Chain::Helpers include Chain::Helpers
def perform! def perform!
if project.pending_delete?
return error('Project is deleted!')
end
unless project.builds_enabled? unless project.builds_enabled?
return error('Pipelines are disabled!') return error('Pipelines are disabled!')
end end

View file

@ -40,21 +40,17 @@ module Gitlab
# - An Array of the unique ::Commit objects in the first value # - An Array of the unique ::Commit objects in the first value
def summarize def summarize
summary = contents summary = contents
.map { |content| build_entry(content) }
.tap { |summary| fill_last_commits!(summary) } .tap { |summary| fill_last_commits!(summary) }
[summary, commits] [summary, commits]
end end
def fetch_logs def fetch_logs
cache_key = ['projects', project.id, 'logs', commit.id, path, offset] logs, _ = summarize
Rails.cache.fetch(cache_key, expires_in: CACHE_EXPIRE_IN) do
logs, _ = summarize
new_offset = next_offset if more? new_offset = next_offset if more?
[logs.as_json, new_offset] [logs.as_json, new_offset]
end
end end
# Does the tree contain more entries after the given offset + limit? # Does the tree contain more entries after the given offset + limit?
@ -71,7 +67,7 @@ module Gitlab
private private
def contents def contents
all_contents[offset, limit] all_contents[offset, limit] || []
end end
def commits def commits
@ -82,22 +78,17 @@ module Gitlab
project.repository project.repository
end end
# Ensure the path is in "path/" format
def ensured_path
File.join(*[path, ""]) if path
end
def entry_path(entry) def entry_path(entry)
File.join(*[path, entry[:file_name]].compact).force_encoding(Encoding::ASCII_8BIT) File.join(*[path, entry[:file_name]].compact).force_encoding(Encoding::ASCII_8BIT)
end end
def build_entry(entry)
{ file_name: entry.name, type: entry.type }
end
def fill_last_commits!(entries) def fill_last_commits!(entries)
# Ensure the path is in "path/" format commits_hsh = fetch_last_cached_commits_list
ensured_path =
if path
File.join(*[path, ""])
end
commits_hsh = repository.list_last_commits_for_tree(commit.id, ensured_path, offset: offset, limit: limit, literal_pathspec: true)
prerender_commit_full_titles!(commits_hsh.values) prerender_commit_full_titles!(commits_hsh.values)
entries.each do |entry| entries.each do |entry|
@ -112,6 +103,18 @@ module Gitlab
end end
end end
def fetch_last_cached_commits_list
cache_key = ['projects', project.id, 'last_commits_list', commit.id, ensured_path, offset, limit]
commits = Rails.cache.fetch(cache_key, expires_in: CACHE_EXPIRE_IN) do
repository
.list_last_commits_for_tree(commit.id, ensured_path, offset: offset, limit: limit, literal_pathspec: true)
.transform_values!(&:to_hash)
end
commits.transform_values! { |value| Commit.from_hash(value, project) }
end
def cache_commit(commit) def cache_commit(commit)
return unless commit.present? return unless commit.present?
@ -123,12 +126,18 @@ module Gitlab
end end
def all_contents def all_contents
strong_memoize(:all_contents) do strong_memoize(:all_contents) { cached_contents }
end
def cached_contents
cache_key = ['projects', project.id, 'content', commit.id, path]
Rails.cache.fetch(cache_key, expires_in: CACHE_EXPIRE_IN) do
[ [
*tree.trees, *tree.trees,
*tree.blobs, *tree.blobs,
*tree.submodules *tree.submodules
] ].map { |entry| { file_name: entry.name, type: entry.type } }
end end
end end

View file

@ -1762,6 +1762,9 @@ msgstr ""
msgid "AddContextCommits|Add/remove" msgid "AddContextCommits|Add/remove"
msgstr "" msgstr ""
msgid "AddMember|Invite limit of %{daily_invites} per day exceeded"
msgstr ""
msgid "AddMember|No users specified." msgid "AddMember|No users specified."
msgstr "" msgstr ""

View file

@ -9,6 +9,10 @@ RSpec.describe Groups::GroupLinksController do
let(:group_member) { create(:user) } let(:group_member) { create(:user) }
let!(:project) { create(:project, group: shared_group) } let!(:project) { create(:project, group: shared_group) }
around do |example|
travel_to DateTime.new(2019, 4, 1) { example.run }
end
before do before do
sign_in(user) sign_in(user)

View file

@ -9,6 +9,10 @@ RSpec.describe Groups::GroupMembersController do
let(:group) { create(:group, :public) } let(:group) { create(:group, :public) }
let(:membership) { create(:group_member, group: group) } let(:membership) { create(:group_member, group: group) }
around do |example|
travel_to DateTime.new(2019, 4, 1) { example.run }
end
describe 'GET index' do describe 'GET index' do
it 'renders index with 200 status code' do it 'renders index with 200 status code' do
get :index, params: { group_id: group } get :index, params: { group_id: group }

View file

@ -8,6 +8,10 @@ RSpec.describe Projects::GroupLinksController do
let_it_be(:project) { create(:project, :private, group: group2) } let_it_be(:project) { create(:project, :private, group: group2) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
around do |example|
travel_to DateTime.new(2019, 4, 1) { example.run }
end
before do before do
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)

View file

@ -7,6 +7,10 @@ RSpec.describe Projects::ProjectMembersController do
let(:group) { create(:group, :public) } let(:group) { create(:group, :public) }
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
around do |example|
travel_to DateTime.new(2019, 4, 1) { example.run }
end
describe 'GET index' do describe 'GET index' do
it 'has the project_members address with a 200 status code' do it 'has the project_members address with a 200 status code' do
get :index, params: { namespace_id: project.namespace, project_id: project } get :index, params: { namespace_id: project.namespace, project_id: project }

View file

@ -56,18 +56,6 @@ RSpec.describe Projects::RefsController do
expect(response).to be_successful expect(response).to be_successful
expect(json_response).to be_kind_of(Array) expect(json_response).to be_kind_of(Array)
end end
it 'caches tree summary data', :use_clean_rails_memory_store_caching do
expect_next_instance_of(::Gitlab::TreeSummary) do |instance|
expect(instance).to receive_messages(summarize: ['logs'], next_offset: 50, more?: true)
end
xhr_get(:json, offset: 25)
cache_key = "projects/#{project.id}/logs/#{project.commit.id}/#{path}/25"
expect(Rails.cache.fetch(cache_key)).to eq(['logs', 50])
expect(response.headers['More-Logs-Offset']).to eq("50")
end
end end
end end
end end

View file

@ -15,7 +15,7 @@ FactoryBot.define do
trait(:invited) do trait(:invited) do
user_id { nil } user_id { nil }
invite_token { 'xxx' } invite_token { 'xxx' }
invite_email { 'email@email.com' } sequence(:invite_email) { |n| "email#{n}@email.com" }
end end
trait :blocked do trait :blocked do

View file

@ -74,6 +74,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do
it 'does not break the chain' do it 'does not break the chain' do
expect(step.break?).to eq false expect(step.break?).to eq false
end end
context 'when project is deleted' do
before do
project.update!(pending_delete: true)
end
specify { expect(step.perform!).to contain_exactly('Project is deleted!') }
end
end end
describe '#allowed_to_write_ref?' do describe '#allowed_to_write_ref?' do

View file

@ -3,6 +3,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::TreeSummary do RSpec.describe Gitlab::TreeSummary do
include RepoHelpers
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let(:project) { create(:project, :empty_repo) } let(:project) { create(:project, :empty_repo) }
@ -44,6 +45,40 @@ RSpec.describe Gitlab::TreeSummary do
expect(commits).to match_array(entries.map { |entry| entry[:commit] }) expect(commits).to match_array(entries.map { |entry| entry[:commit] })
end end
end end
context 'when offset is over the limit' do
let(:offset) { 100 }
it 'returns an empty array' do
expect(summarized).to eq([[], []])
end
end
context 'with caching', :use_clean_rails_memory_store_caching do
subject { Rails.cache.fetch(key) }
before do
summarized
end
context 'Repository tree cache' do
let(:key) { ['projects', project.id, 'content', commit.id, path] }
it 'creates a cache for repository content' do
is_expected.to eq([{ file_name: 'a.txt', type: :blob }])
end
end
context 'Commits list cache' do
let(:offset) { 0 }
let(:limit) { 25 }
let(:key) { ['projects', project.id, 'last_commits_list', commit.id, path, offset, limit] }
it 'creates a cache for commits list' do
is_expected.to eq('a.txt' => commit.to_hash)
end
end
end
end end
describe '#summarize (entries)' do describe '#summarize (entries)' do
@ -167,6 +202,46 @@ RSpec.describe Gitlab::TreeSummary do
end end
end end
describe 'References in commit messages' do
let_it_be(:project) { create(:project, :empty_repo) }
let_it_be(:issue) { create(:issue, project: project) }
let(:entries) { summary.summarize.first }
let(:entry) { entries.find { |entry| entry[:file_name] == 'issue.txt' } }
before_all do
create_file_in_repo(project, 'master', 'master', 'issue.txt', '', commit_message: "Issue ##{issue.iid}")
end
where(:project_visibility, :user_role, :issue_confidential, :expected_result) do
'private' | :guest | false | true
'private' | :guest | true | false
'private' | :reporter | false | true
'private' | :reporter | true | true
'internal' | :guest | false | true
'internal' | :guest | true | false
'internal' | :reporter | false | true
'internal' | :reporter | true | true
'public' | :guest | false | true
'public' | :guest | true | false
'public' | :reporter | false | true
'public' | :reporter | true | true
end
with_them do
subject { entry[:commit_title_html].include?("title=\"#{issue.title}\"") }
before do
project.add_role(user, user_role)
project.update!(visibility_level: Gitlab::VisibilityLevel.level_value(project_visibility))
issue.update!(confidential: issue_confidential)
end
it { is_expected.to eq(expected_result) }
end
end
describe '#more?' do describe '#more?' do
let(:path) { 'tmp/more' } let(:path) { 'tmp/more' }

View file

@ -0,0 +1,55 @@
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20201007033723_insert_daily_invites_plan_limits.rb')
RSpec.describe InsertDailyInvitesPlanLimits do
let(:plans) { table(:plans) }
let(:plan_limits) { table(:plan_limits) }
let!(:free_plan) { plans.create!(name: 'free') }
let!(:bronze_plan) { plans.create!(name: 'bronze') }
let!(:silver_plan) { plans.create!(name: 'silver') }
let!(:gold_plan) { plans.create!(name: 'gold') }
context 'when on Gitlab.com' do
before do
expect(Gitlab).to receive(:com?).at_most(:twice).and_return(true)
end
it 'correctly migrates up and down' do
reversible_migration do |migration|
migration.before -> {
expect(plan_limits.where.not(daily_invites: 0)).to be_empty
}
# Expectations will run after the up migration.
migration.after -> {
expect(plan_limits.pluck(:plan_id, :daily_invites)).to contain_exactly(
[free_plan.id, 20],
[bronze_plan.id, 0],
[silver_plan.id, 0],
[gold_plan.id, 0]
)
}
end
end
end
context 'when on self hosted' do
before do
expect(Gitlab).to receive(:com?).at_most(:twice).and_return(false)
end
it 'correctly migrates up and down' do
reversible_migration do |migration|
migration.before -> {
expect(plan_limits.pluck(:daily_invites)).to eq []
}
migration.after -> {
expect(plan_limits.pluck(:daily_invites)).to eq []
}
end
end
end
end

View file

@ -34,6 +34,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
it { is_expected.to have_many(:auto_canceled_jobs) } it { is_expected.to have_many(:auto_canceled_jobs) }
it { is_expected.to have_many(:sourced_pipelines) } it { is_expected.to have_many(:sourced_pipelines) }
it { is_expected.to have_many(:triggered_pipelines) } it { is_expected.to have_many(:triggered_pipelines) }
it { is_expected.to have_many(:pipeline_artifacts) }
it { is_expected.to have_one(:chat_data) } it { is_expected.to have_one(:chat_data) }
it { is_expected.to have_one(:source_pipeline) } it { is_expected.to have_one(:source_pipeline) }
@ -41,14 +42,15 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
it { is_expected.to have_one(:source_job) } it { is_expected.to have_one(:source_job) }
it { is_expected.to have_one(:pipeline_config) } it { is_expected.to have_one(:pipeline_config) }
it { is_expected.to validate_presence_of(:sha) }
it { is_expected.to validate_presence_of(:status) }
it { is_expected.to respond_to :git_author_name } it { is_expected.to respond_to :git_author_name }
it { is_expected.to respond_to :git_author_email } it { is_expected.to respond_to :git_author_email }
it { is_expected.to respond_to :short_sha } it { is_expected.to respond_to :short_sha }
it { is_expected.to delegate_method(:full_path).to(:project).with_prefix } it { is_expected.to delegate_method(:full_path).to(:project).with_prefix }
it { is_expected.to have_many(:pipeline_artifacts) }
describe 'validations' do
it { is_expected.to validate_presence_of(:sha) }
it { is_expected.to validate_presence_of(:status) }
end
describe 'associations' do describe 'associations' do
it 'has a bidirectional relationship with projects' do it 'has a bidirectional relationship with projects' do

View file

@ -171,6 +171,43 @@ RSpec.describe Member do
end end
end end
describe '.in_hierarchy' do
let(:root_ancestor) { create(:group) }
let(:project) { create(:project, group: root_ancestor) }
let(:subgroup) { create(:group, parent: root_ancestor) }
let(:subgroup_project) { create(:project, group: subgroup) }
let!(:root_ancestor_member) { create(:group_member, group: root_ancestor) }
let!(:project_member) { create(:project_member, project: project) }
let!(:subgroup_member) { create(:group_member, group: subgroup) }
let!(:subgroup_project_member) { create(:project_member, project: subgroup_project) }
let(:hierarchy_members) do
[
root_ancestor_member,
project_member,
subgroup_member,
subgroup_project_member
]
end
subject { Member.in_hierarchy(project) }
it { is_expected.to contain_exactly(*hierarchy_members) }
context 'with scope prefix' do
subject { Member.where.not(source: project).in_hierarchy(subgroup) }
it { is_expected.to contain_exactly(root_ancestor_member, subgroup_member, subgroup_project_member) }
end
context 'with scope suffix' do
subject { Member.in_hierarchy(project).where.not(source: project) }
it { is_expected.to contain_exactly(root_ancestor_member, subgroup_member, subgroup_project_member) }
end
end
describe '.invite' do describe '.invite' do
it { expect(described_class.invite).not_to include @maintainer } it { expect(described_class.invite).not_to include @maintainer }
it { expect(described_class.invite).to include @invited_member } it { expect(described_class.invite).to include @invited_member }
@ -251,6 +288,21 @@ RSpec.describe Member do
it { is_expected.to include(expiring_tomorrow, not_expiring) } it { is_expected.to include(expiring_tomorrow, not_expiring) }
end end
describe '.created_today' do
let_it_be(:now) { Time.current }
let_it_be(:created_today) { create(:group_member, created_at: now.beginning_of_day) }
let_it_be(:created_yesterday) { create(:group_member, created_at: now - 1.day) }
before do
travel_to now
end
subject { described_class.created_today }
it { is_expected.not_to include(created_yesterday) }
it { is_expected.to include(created_today) }
end
describe '.last_ten_days_excluding_today' do describe '.last_ten_days_excluding_today' do
let_it_be(:now) { Time.current } let_it_be(:now) { Time.current }
let_it_be(:created_today) { create(:group_member, created_at: now.beginning_of_day) } let_it_be(:created_today) { create(:group_member, created_at: now.beginning_of_day) }

View file

@ -209,6 +209,7 @@ RSpec.describe PlanLimits do
ci_pipeline_size ci_pipeline_size
ci_active_jobs ci_active_jobs
storage_size_limit storage_size_limit
daily_invites
] + disabled_max_artifact_size_columns ] + disabled_max_artifact_size_columns
end end

View file

@ -2,11 +2,13 @@
require 'spec_helper' require 'spec_helper'
require 'googleauth'
RSpec.describe PrometheusService, :use_clean_rails_memory_store_caching, :snowplow do RSpec.describe PrometheusService, :use_clean_rails_memory_store_caching, :snowplow do
include PrometheusHelpers include PrometheusHelpers
include ReactiveCachingHelpers include ReactiveCachingHelpers
let(:project) { create(:prometheus_project) } let_it_be_with_reload(:project) { create(:prometheus_project) }
let(:service) { project.prometheus_service } let(:service) { project.prometheus_service }
describe "Associations" do describe "Associations" do
@ -256,19 +258,66 @@ RSpec.describe PrometheusService, :use_clean_rails_memory_store_caching, :snowpl
context 'behind IAP' do context 'behind IAP' do
let(:manual_configuration) { true } let(:manual_configuration) { true }
before do let(:google_iap_service_account) do
# dummy private key generated only for this test to pass openssl validation {
service.google_iap_service_account_json = '{"type":"service_account","private_key":"-----BEGIN RSA PRIVATE KEY-----\nMIIBOAIBAAJAU85LgUY5o6j6j/07GMLCNUcWJOBA1buZnNgKELayA6mSsHrIv31J\nY8kS+9WzGPQninea7DcM4hHA7smMgQD1BwIDAQABAkAqKxMy6PL3tn7dFL43p0ex\nJyOtSmlVIiAZG1t1LXhE/uoLpYi5DnbYqGgu0oih+7nzLY/dXpNpXUmiRMOUEKmB\nAiEAoTi2rBXbrLSi2C+H7M/nTOjMQQDuZ8Wr4uWpKcjYJTMCIQCFEskL565oFl/7\nRRQVH+cARrAsAAoJSbrOBAvYZ0PI3QIgIEFwis10vgEF86rOzxppdIG/G+JL0IdD\n9IluZuXAGPECIGUo7qSaLr75o2VEEgwtAFH5aptIPFjrL5LFCKwtdB4RAiAYZgFV\nHCMmaooAw/eELuMoMWNYmujZ7VaAnOewGDW0uw==\n-----END RSA PRIVATE KEY-----\n"}' type: "service_account",
service.google_iap_audience_client_id = "IAP_CLIENT_ID.apps.googleusercontent.com" # dummy private key generated only for this test to pass openssl validation
private_key: <<~KEY
-----BEGIN RSA PRIVATE KEY-----
MIIBOAIBAAJAU85LgUY5o6j6j/07GMLCNUcWJOBA1buZnNgKELayA6mSsHrIv31J
Y8kS+9WzGPQninea7DcM4hHA7smMgQD1BwIDAQABAkAqKxMy6PL3tn7dFL43p0ex
JyOtSmlVIiAZG1t1LXhE/uoLpYi5DnbYqGgu0oih+7nzLY/dXpNpXUmiRMOUEKmB
AiEAoTi2rBXbrLSi2C+H7M/nTOjMQQDuZ8Wr4uWpKcjYJTMCIQCFEskL565oFl/7
RRQVH+cARrAsAAoJSbrOBAvYZ0PI3QIgIEFwis10vgEF86rOzxppdIG/G+JL0IdD
9IluZuXAGPECIGUo7qSaLr75o2VEEgwtAFH5aptIPFjrL5LFCKwtdB4RAiAYZgFV
HCMmaooAw/eELuMoMWNYmujZ7VaAnOewGDW0uw==
-----END RSA PRIVATE KEY-----
KEY
}
end
stub_request(:post, "https://oauth2.googleapis.com/token").to_return(status: 200, body: '{"id_token": "FOO"}', headers: { 'Content-Type': 'application/json; charset=UTF-8' }) def stub_iap_request
service.google_iap_service_account_json = Gitlab::Json.generate(google_iap_service_account)
service.google_iap_audience_client_id = 'IAP_CLIENT_ID.apps.googleusercontent.com'
stub_request(:post, 'https://oauth2.googleapis.com/token')
.to_return(
status: 200,
body: '{"id_token": "FOO"}',
headers: { 'Content-Type': 'application/json; charset=UTF-8' }
)
end end
it 'includes the authorization header' do it 'includes the authorization header' do
stub_iap_request
expect(service.prometheus_client).not_to be_nil expect(service.prometheus_client).not_to be_nil
expect(service.prometheus_client.send(:options)).to have_key(:headers) expect(service.prometheus_client.send(:options)).to have_key(:headers)
expect(service.prometheus_client.send(:options)[:headers]).to eq(authorization: "Bearer FOO") expect(service.prometheus_client.send(:options)[:headers]).to eq(authorization: "Bearer FOO")
end end
context 'when passed with token_credential_uri', issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/284819' do
let(:malicious_host) { 'http://example.com' }
where(:param_name) do
[
:token_credential_uri,
:tokencredentialuri,
:Token_credential_uri,
:tokenCredentialUri
]
end
with_them do
it 'does not make any unexpected HTTP requests' do
google_iap_service_account[param_name] = malicious_host
stub_iap_request
stub_request(:any, malicious_host).to_raise('Making additional HTTP requests is forbidden!')
expect(service.prometheus_client).not_to be_nil
end
end
end
end end
end end

View file

@ -4,91 +4,136 @@ require 'spec_helper'
RSpec.describe API::Lint do RSpec.describe API::Lint do
describe 'POST /ci/lint' do describe 'POST /ci/lint' do
context 'with valid .gitlab-ci.yaml content' do context 'when signup settings are disabled' do
let(:yaml_content) do Gitlab::CurrentSettings.signup_enabled = false
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
context 'when unauthenticated' do
it 'returns authentication error' do
post api('/ci/lint'), params: { content: 'content' }
expect(response).to have_gitlab_http_status(:unauthorized)
end
end end
it 'passes validation without warnings or errors' do context 'when authenticated' do
post api('/ci/lint'), params: { content: yaml_content } it 'returns unauthorized error' do
post api('/ci/lint'), params: { content: 'content' }
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:unauthorized)
expect(json_response).to be_an Hash end
expect(json_response['status']).to eq('valid')
expect(json_response['warnings']).to eq([])
expect(json_response['errors']).to eq([])
end
it 'outputs expanded yaml content' do
post api('/ci/lint'), params: { content: yaml_content, include_merged_yaml: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to have_key('merged_yaml')
end end
end end
context 'with valid .gitlab-ci.yaml with warnings' do context 'when signup settings are enabled' do
let(:yaml_content) { { job: { script: 'ls', rules: [{ when: 'always' }] } }.to_yaml } Gitlab::CurrentSettings.signup_enabled = true
it 'passes validation but returns warnings' do context 'when unauthenticated' do
post api('/ci/lint'), params: { content: yaml_content } it 'returns authentication error' do
post api('/ci/lint'), params: { content: 'content' }
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:unauthorized)
expect(json_response['status']).to eq('valid') end
expect(json_response['warnings']).not_to be_empty
expect(json_response['status']).to eq('valid')
expect(json_response['errors']).to eq([])
end end
end
context 'with an invalid .gitlab_ci.yml' do context 'when authenticated' do
context 'with invalid syntax' do let_it_be(:api_user) { create(:user) }
let(:yaml_content) { 'invalid content' } it 'returns authentication success' do
post api('/ci/lint', api_user), params: { content: 'content' }
it 'responds with errors about invalid syntax' do
post api('/ci/lint'), params: { content: yaml_content }
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['status']).to eq('invalid') end
end
end
context 'when authenticated' do
let_it_be(:api_user) { create(:user) }
context 'with valid .gitlab-ci.yaml content' do
let(:yaml_content) do
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
end
it 'passes validation without warnings or errors' do
post api('/ci/lint', api_user), params: { content: yaml_content }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Hash
expect(json_response['status']).to eq('valid')
expect(json_response['warnings']).to eq([]) expect(json_response['warnings']).to eq([])
expect(json_response['errors']).to eq(['Invalid configuration format']) expect(json_response['errors']).to eq([])
end end
it 'outputs expanded yaml content' do it 'outputs expanded yaml content' do
post api('/ci/lint'), params: { content: yaml_content, include_merged_yaml: true } post api('/ci/lint', api_user), params: { content: yaml_content, include_merged_yaml: true }
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to have_key('merged_yaml') expect(json_response).to have_key('merged_yaml')
end end
end end
context 'with invalid configuration' do context 'with valid .gitlab-ci.yaml with warnings' do
let(:yaml_content) { '{ image: "ruby:2.7", services: ["postgres"] }' } let(:yaml_content) { { job: { script: 'ls', rules: [{ when: 'always' }] } }.to_yaml }
it 'responds with errors about invalid configuration' do it 'passes validation but returns warnings' do
post api('/ci/lint'), params: { content: yaml_content } post api('/ci/lint', api_user), params: { content: yaml_content }
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['status']).to eq('invalid') expect(json_response['status']).to eq('valid')
expect(json_response['warnings']).to eq([]) expect(json_response['warnings']).not_to be_empty
expect(json_response['errors']).to eq(['jobs config should contain at least one visible job']) expect(json_response['status']).to eq('valid')
end expect(json_response['errors']).to eq([])
it 'outputs expanded yaml content' do
post api('/ci/lint'), params: { content: yaml_content, include_merged_yaml: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to have_key('merged_yaml')
end end
end end
end
context 'without the content parameter' do context 'with an invalid .gitlab_ci.yml' do
it 'responds with validation error about missing content' do context 'with invalid syntax' do
post api('/ci/lint') let(:yaml_content) { 'invalid content' }
expect(response).to have_gitlab_http_status(:bad_request) it 'responds with errors about invalid syntax' do
expect(json_response['error']).to eq('content is missing') post api('/ci/lint', api_user), params: { content: yaml_content }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['status']).to eq('invalid')
expect(json_response['warnings']).to eq([])
expect(json_response['errors']).to eq(['Invalid configuration format'])
end
it 'outputs expanded yaml content' do
post api('/ci/lint', api_user), params: { content: yaml_content, include_merged_yaml: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to have_key('merged_yaml')
end
end
context 'with invalid configuration' do
let(:yaml_content) { '{ image: "ruby:2.7", services: ["postgres"] }' }
it 'responds with errors about invalid configuration' do
post api('/ci/lint', api_user), params: { content: yaml_content }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['status']).to eq('invalid')
expect(json_response['warnings']).to eq([])
expect(json_response['errors']).to eq(['jobs config should contain at least one visible job'])
end
it 'outputs expanded yaml content' do
post api('/ci/lint', api_user), params: { content: yaml_content, include_merged_yaml: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to have_key('merged_yaml')
end
end
end
context 'without the content parameter' do
it 'responds with validation error about missing content' do
post api('/ci/lint', api_user)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('content is missing')
end
end end
end end
end end
@ -364,6 +409,18 @@ RSpec.describe API::Lint do
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
context 'when project is public' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
end
it 'returns authentication error' do
ci_lint
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end end
context 'when authenticated as non-member' do context 'when authenticated as non-member' do
@ -387,13 +444,10 @@ RSpec.describe API::Lint do
context 'when running as dry run' do context 'when running as dry run' do
let(:dry_run) { true } let(:dry_run) { true }
it 'returns pipeline creation error' do it 'returns authentication error' do
ci_lint ci_lint
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['merged_yaml']).to eq(nil)
expect(json_response['valid']).to eq(false)
expect(json_response['errors']).to eq(['Insufficient permissions to create a new pipeline'])
end end
end end
@ -410,7 +464,11 @@ RSpec.describe API::Lint do
) )
end end
it_behaves_like 'valid project config' it 'returns authentication error' do
ci_lint
expect(response).to have_gitlab_http_status(:forbidden)
end
end end
end end
end end

View file

@ -21,6 +21,12 @@ RSpec.describe API::MergeRequestApprovals do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
context 'when merge request author has only guest access' do
it_behaves_like 'rejects user from accessing merge request info' do
let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/approvals" }
end
end
end end
describe 'POST :id/merge_requests/:merge_request_iid/approve' do describe 'POST :id/merge_requests/:merge_request_iid/approve' do

View file

@ -35,6 +35,12 @@ RSpec.describe API::MergeRequestDiffs, 'MergeRequestDiffs' do
get api("/projects/#{project.id}/merge_requests/0/versions", user) get api("/projects/#{project.id}/merge_requests/0/versions", user)
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
context 'when merge request author has only guest access' do
it_behaves_like 'rejects user from accessing merge request info' do
let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions" }
end
end
end end
describe 'GET /projects/:id/merge_requests/:merge_request_iid/versions/:version_id' do describe 'GET /projects/:id/merge_requests/:merge_request_iid/versions/:version_id' do
@ -63,5 +69,11 @@ RSpec.describe API::MergeRequestDiffs, 'MergeRequestDiffs' do
get api("/projects/#{project.id}/merge_requests/#{non_existing_record_iid}/versions/#{merge_request_diff.id}", user) get api("/projects/#{project.id}/merge_requests/#{non_existing_record_iid}/versions/#{merge_request_diff.id}", user)
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
context 'when merge request author has only guest access' do
it_behaves_like 'rejects user from accessing merge request info' do
let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/#{merge_request_diff.id}" }
end
end
end end
end end

View file

@ -1087,6 +1087,12 @@ RSpec.describe API::MergeRequests do
end end
end end
context 'when merge request author has only guest access' do
it_behaves_like 'rejects user from accessing merge request info' do
let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}" }
end
end
context 'merge_request_metrics' do context 'merge_request_metrics' do
let(:pipeline) { create(:ci_empty_pipeline) } let(:pipeline) { create(:ci_empty_pipeline) }
@ -1263,6 +1269,12 @@ RSpec.describe API::MergeRequests do
it_behaves_like 'issuable participants endpoint' do it_behaves_like 'issuable participants endpoint' do
let(:entity) { create(:merge_request, :simple, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, source_branch: 'markdown', title: "Test", created_at: base_time) } let(:entity) { create(:merge_request, :simple, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, source_branch: 'markdown', title: "Test", created_at: base_time) }
end end
context 'when merge request author has only guest access' do
it_behaves_like 'rejects user from accessing merge request info' do
let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/participants" }
end
end
end end
describe 'GET /projects/:id/merge_requests/:merge_request_iid/commits' do describe 'GET /projects/:id/merge_requests/:merge_request_iid/commits' do
@ -1288,6 +1300,12 @@ RSpec.describe API::MergeRequests do
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
context 'when merge request author has only guest access' do
it_behaves_like 'rejects user from accessing merge request info' do
let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/commits" }
end
end
end end
describe 'GET /projects/:id/merge_requests/:merge_request_iid/:context_commits' do describe 'GET /projects/:id/merge_requests/:merge_request_iid/:context_commits' do
@ -1363,6 +1381,12 @@ RSpec.describe API::MergeRequests do
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
context 'when merge request author has only guest access' do
it_behaves_like 'rejects user from accessing merge request info' do
let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/changes" }
end
end
it_behaves_like 'find an existing merge request' it_behaves_like 'find an existing merge request'
it_behaves_like 'accesses diffs via raw_diffs' it_behaves_like 'accesses diffs via raw_diffs'
@ -1452,6 +1476,12 @@ RSpec.describe API::MergeRequests do
expect(response).to have_gitlab_http_status(:forbidden) expect(response).to have_gitlab_http_status(:forbidden)
end end
end end
context 'when merge request author has only guest access' do
it_behaves_like 'rejects user from accessing merge request info' do
let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/pipelines" }
end
end
end end
describe 'POST /projects/:id/merge_requests/:merge_request_iid/pipelines' do describe 'POST /projects/:id/merge_requests/:merge_request_iid/pipelines' do

View file

@ -329,6 +329,14 @@ RSpec.describe API::Todos do
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
end end
it 'returns an error if the issuable author does not have access' do
project_1.add_guest(issuable.author)
post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", issuable.author)
expect(response).to have_gitlab_http_status(:not_found)
end
end end
describe 'POST :id/issuable_type/:issueable_id/todo' do describe 'POST :id/issuable_type/:issueable_id/todo' do

View file

@ -0,0 +1,42 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::AbortProjectPipelinesService do
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, :running, project: project) }
let_it_be(:build) { create(:ci_build, :running, pipeline: pipeline) }
describe '#execute' do
it 'cancels all running pipelines and related jobs' do
result = described_class.new.execute(project)
expect(result).to be_success
expect(pipeline.reload).to be_canceled
expect(build.reload).to be_canceled
end
it 'avoids N+1 queries' do
control_count = ActiveRecord::QueryRecorder.new { described_class.new.execute(project) }.count
pipelines = create_list(:ci_pipeline, 5, :running, project: project)
create_list(:ci_build, 5, :running, pipeline: pipelines.first)
expect { described_class.new.execute(project) }.not_to exceed_query_limit(control_count)
end
end
context 'when feature disabled' do
before do
stub_feature_flags(abort_deleted_project_pipelines: false)
end
it 'does not abort the pipeline' do
result = described_class.new.execute(project)
expect(result).to be(nil)
expect(pipeline.reload).to be_running
expect(build.reload).to be_running
end
end
end

View file

@ -69,6 +69,12 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
destroy_project(project, user, {}) destroy_project(project, user, {})
end end
it 'performs cancel for project ci pipelines' do
expect(::Ci::AbortProjectPipelinesService).to receive_message_chain(:new, :execute).with(project)
destroy_project(project, user, {})
end
context 'when project has remote mirrors' do context 'when project has remote mirrors' do
let!(:project) do let!(:project) do
create(:project, :repository, namespace: user.namespace).tap do |project| create(:project, :repository, namespace: user.namespace).tap do |project|

View file

@ -0,0 +1,23 @@
# frozen_string_literal: true
RSpec.shared_examples 'rejects user from accessing merge request info' do
let(:project) { create(:project, :private) }
let(:merge_request) do
create(:merge_request,
author: user,
source_project: project,
target_project: project
)
end
before do
project.add_guest(user)
end
it 'returns a 404 error' do
get api(url, user)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Merge Request Not Found')
end
end