New upstream version 13.6.7
This commit is contained in:
parent
3571a16ade
commit
eae3f48ed8
50 changed files with 720 additions and 123 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -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)
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
13.6.6
|
13.6.7
|
|
@ -1 +1 @@
|
||||||
13.13.0
|
13.13.1
|
||||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
||||||
13.6.6
|
13.6.7
|
|
@ -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 }]
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
|
@ -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') }
|
||||||
|
|
|
@ -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
|
||||||
|
|
25
app/services/ci/abort_project_pipelines_service.rb
Normal file
25
app/services/ci/abort_project_pipelines_service.rb
Normal 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
|
|
@ -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')
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
|
@ -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
|
|
@ -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
|
1
db/schema_migrations/20201007033527
Normal file
1
db/schema_migrations/20201007033527
Normal file
|
@ -0,0 +1 @@
|
||||||
|
1200747265d5095a86250020786d6f1e9e50bc75328a71de497046807afa89d7
|
1
db/schema_migrations/20201007033723
Normal file
1
db/schema_migrations/20201007033723
Normal file
|
@ -0,0 +1 @@
|
||||||
|
febefead6f966960f6493d29add5f35fc4a1080b5118c5526502fa5fe1d29023
|
|
@ -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
|
||||||
|
|
|
@ -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 Gitaly’s 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 Gitaly’s configuration 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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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' }
|
||||||
|
|
||||||
|
|
55
spec/migrations/insert_daily_invites_plan_limits_spec.rb
Normal file
55
spec/migrations/insert_daily_invites_plan_limits_spec.rb
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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) }
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
42
spec/services/ci/abort_project_pipelines_service_spec.rb
Normal file
42
spec/services/ci/abort_project_pipelines_service_spec.rb
Normal 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
|
|
@ -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|
|
||||||
|
|
|
@ -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
|
Loading…
Reference in a new issue