Commit 4aa6c2df authored by GitLab Release Tools Bot's avatar GitLab Release Tools Bot

Merge remote-tracking branch 'dev/master'

parents 15924fc0 e5fc3be1
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
## 13.8.4 (2021-02-11)
### Security (1 change)
- Geo: Pass GL-ID in a JWT token when proxy-push from secondary.
## 13.8.3 (2021-02-05) ## 13.8.3 (2021-02-05)
### Fixed (2 changes) ### Fixed (2 changes)
...@@ -135,6 +142,13 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -135,6 +142,13 @@ Please view this file on the master branch, on stable branches it's out of date.
- Enable DevOps Adoption Report feature flag if any Segments already exist. !51602 - Enable DevOps Adoption Report feature flag if any Segments already exist. !51602
## 13.7.7 (2021-02-11)
### Security (1 change)
- Geo: Pass GL-ID in a JWT token when proxy-push from secondary.
## 13.7.6 (2021-02-01) ## 13.7.6 (2021-02-01)
### Security (2 changes) ### Security (2 changes)
...@@ -324,6 +338,13 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -324,6 +338,13 @@ Please view this file on the master branch, on stable branches it's out of date.
- Rename code coverage analytics sections. !49931 - Rename code coverage analytics sections. !49931
## 13.6.7 (2021-02-11)
### Security (1 change)
- Geo: Pass GL-ID in a JWT token when proxy-push from secondary.
## 13.6.6 (2021-02-01) ## 13.6.6 (2021-02-01)
### Security (2 changes) ### Security (2 changes)
......
...@@ -2,6 +2,21 @@ ...@@ -2,6 +2,21 @@
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.8.4 (2021-02-11)
### Security (9 changes)
- Cancel running and pending jobs when a project is deleted. !1220
- Prevent Denial of Service Attack on gitlab-shell.
- Prevent exposure of confidential issue titles in file browser.
- Updates authorization for linting API.
- Check user access on API merge request read actions.
- Limit daily invitations to groups and projects.
- Enforce the analytics enabled project setting for project-level analytics features.
- Perform SSL verification for FortiTokenCloud Integration.
- Prevent Server-side Request Forgery for Prometheus when secured by Google IAP.
## 13.8.3 (2021-02-05) ## 13.8.3 (2021-02-05)
### Fixed (2 changes) ### Fixed (2 changes)
...@@ -387,6 +402,21 @@ entry. ...@@ -387,6 +402,21 @@ entry.
- Add verbiage + link sast to show it's in core. !51935 - Add verbiage + link sast to show it's in core. !51935
## 13.7.7 (2021-02-11)
### Security (9 changes)
- Cancel running and pending jobs when a project is deleted. !1220
- Prevent Denial of Service Attack on gitlab-shell.
- Prevent exposure of confidential issue titles in file browser.
- Updates authorization for linting API.
- Check user access on API merge request read actions.
- Limit daily invitations to groups and projects.
- Enforce the analytics enabled project setting for project-level analytics features.
- Perform SSL verification for FortiTokenCloud Integration.
- Prevent Server-side Request Forgery for Prometheus when secured by Google IAP.
## 13.7.6 (2021-02-01) ## 13.7.6 (2021-02-01)
### Security (5 changes) ### Security (5 changes)
...@@ -908,6 +938,19 @@ entry. ...@@ -908,6 +938,19 @@ entry.
- Update GitLab Workhorse to v8.57.0. - Update GitLab Workhorse to v8.57.0.
## 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)
......
...@@ -9,6 +9,7 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -9,6 +9,7 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :set_pipeline_path, only: [:show] before_action :set_pipeline_path, only: [:show]
before_action :authorize_read_pipeline! before_action :authorize_read_pipeline!
before_action :authorize_read_build!, only: [:index] before_action :authorize_read_build!, only: [:index]
before_action :authorize_read_analytics!, only: [:charts]
before_action :authorize_create_pipeline!, only: [:new, :create, :config_variables] before_action :authorize_create_pipeline!, only: [:new, :create, :config_variables]
before_action :authorize_update_pipeline!, only: [:retry, :cancel] before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action do before_action do
......
...@@ -16,6 +16,7 @@ module Ci ...@@ -16,6 +16,7 @@ module Ci
include ShaAttribute include ShaAttribute
include FromUnion include FromUnion
include UpdatedAtFilterable include UpdatedAtFilterable
include EachBatch
MAX_OPEN_MERGE_REQUESTS_REFS = 4 MAX_OPEN_MERGE_REQUESTS_REFS = 4
......
...@@ -55,6 +55,7 @@ class CommitStatus < ApplicationRecord ...@@ -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)))
......
...@@ -47,6 +47,19 @@ class Member < ApplicationRecord ...@@ -47,6 +47,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:
# #
...@@ -79,12 +92,18 @@ class Member < ApplicationRecord ...@@ -79,12 +92,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 ...@@ -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
...@@ -221,6 +221,7 @@ class ProjectPolicy < BasePolicy ...@@ -221,6 +221,7 @@ class ProjectPolicy < BasePolicy
enable :read_pages_content enable :read_pages_content
enable :read_release enable :read_release
enable :read_analytics enable :read_analytics
enable :read_insights
end end
# These abilities are not allowed to admins that are not members of the project, # These abilities are not allowed to admins that are not members of the project,
...@@ -450,6 +451,9 @@ class ProjectPolicy < BasePolicy ...@@ -450,6 +451,9 @@ class ProjectPolicy < BasePolicy
rule { analytics_disabled }.policy do rule { analytics_disabled }.policy do
prevent(:read_analytics) prevent(:read_analytics)
prevent(:read_insights)
prevent(:read_cycle_analytics)
prevent(:read_repository_graphs)
end end
rule { wiki_disabled }.policy do rule { wiki_disabled }.policy do
...@@ -523,6 +527,7 @@ class ProjectPolicy < BasePolicy ...@@ -523,6 +527,7 @@ class ProjectPolicy < BasePolicy
enable :read_cycle_analytics enable :read_cycle_analytics
enable :read_pages_content enable :read_pages_content
enable :read_analytics enable :read_analytics
enable :read_insights
# NOTE: may be overridden by IssuePolicy # NOTE: may be overridden by IssuePolicy
enable :read_issue enable :read_issue
......
# 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: :yaml)
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 ...@@ -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 @@ ...@@ -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
...@@ -47,6 +47,13 @@ module Members ...@@ -47,6 +47,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 ...@@ -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)
......
---
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
# 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
# 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
1200747265d5095a86250020786d6f1e9e50bc75328a71de497046807afa89d7
\ No newline at end of file
febefead6f966960f6493d29add5f35fc4a1080b5118c5526502fa5fe1d29023
\ No newline at end of file
...@@ -15506,6 +15506,7 @@ CREATE TABLE plan_limits ( ...@@ -15506,6 +15506,7 @@ CREATE TABLE plan_limits (
ci_max_artifact_size_api_fuzzing integer DEFAULT 0 NOT NULL, ci_max_artifact_size_api_fuzzing integer DEFAULT 0 NOT NULL,
ci_pipeline_deployments integer DEFAULT 500 NOT NULL, ci_pipeline_deployments integer DEFAULT 500 NOT NULL,
pull_mirror_interval_seconds integer DEFAULT 300 NOT NULL, pull_mirror_interval_seconds integer DEFAULT 300 NOT NULL,
daily_invites integer DEFAULT 0 NOT NULL,
rubygems_max_file_size bigint DEFAULT '3221225472'::bigint NOT NULL rubygems_max_file_size bigint DEFAULT '3221225472'::bigint NOT NULL
); );
......
...@@ -96,6 +96,13 @@ Read more on the [Rack Attack initializer](../security/rack_attack.md) method of ...@@ -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.
......
...@@ -198,6 +198,8 @@ service account can be found at Google's documentation for ...@@ -198,6 +198,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)
......
...@@ -35,11 +35,17 @@ module EE ...@@ -35,11 +35,17 @@ module EE
end end
def geo_push_user def geo_push_user
@geo_push_user ||= ::Geo::PushUser.new_from_headers(request.headers) return unless geo_gl_id
@geo_push_user ||= ::Geo::PushUser.new(geo_gl_id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def geo_gl_id
decoded_authorization&.dig(:gl_id)
end end
def geo_push_user_headers_provided? def geo_push_proxy_request?
::Geo::PushUser.needed_headers_provided?(request.headers) geo_gl_id
end end
def geo_request? def geo_request?
...@@ -53,8 +59,8 @@ module EE ...@@ -53,8 +59,8 @@ module EE
override :access_actor override :access_actor
def access_actor def access_actor
return super unless geo? return super unless geo?
return :geo unless geo_push_user_headers_provided? return :geo unless geo_push_proxy_request?
return geo_push_user.user if geo_push_user.user return geo_push_user.user if geo_push_user&.user
raise ::Gitlab::GitAccess::ForbiddenError, 'Geo push user is invalid.' raise ::Gitlab::GitAccess::ForbiddenError, 'Geo push user is invalid.'
end end
......
...@@ -29,10 +29,6 @@ class Projects::Analytics::IssuesAnalyticsController < Projects::ApplicationCont ...@@ -29,10 +29,6 @@ class Projects::Analytics::IssuesAnalyticsController < Projects::ApplicationCont
private private
def authorize_read_issue_analytics!
render_404 unless project.feature_available?(:issues_analytics)
end
def finder_type def finder_type
IssuesFinder IssuesFinder
end end
......
...@@ -7,6 +7,7 @@ class Projects::InsightsController < Projects::ApplicationController ...@@ -7,6 +7,7 @@ class Projects::InsightsController < Projects::ApplicationController
helper_method :project_insights_config helper_method :project_insights_config
before_action :authorize_read_project! before_action :authorize_read_project!
before_action :authorize_read_insights!
track_unique_visits :show, target_id: 'p_analytics_insights' track_unique_visits :show, target_id: 'p_analytics_insights'
......
...@@ -7,16 +7,6 @@ class Geo::PushUser ...@@ -7,16 +7,6 @@ class Geo::PushUser
@gl_id = gl_id @gl_id = gl_id
end end
def self.needed_headers_provided?(headers)
headers['Geo-GL-Id'].present?
end
def self.new_from_headers(headers)
return unless needed_headers_provided?(headers)
new(headers['Geo-GL-Id'])
end
def user def user
@user ||= identify_using_ssh_key(gl_id) @user ||= identify_using_ssh_key(gl_id)
end end
......
...@@ -151,6 +151,11 @@ module EE ...@@ -151,6 +151,11 @@ module EE
@subject.feature_available?(:code_review_analytics, @user) @subject.feature_available?(:code_review_analytics, @user)
end end
with_scope :subject
condition(:issue_analytics_enabled) do
@subject.feature_available?(:issues_analytics, @user)
end
condition(:status_page_available) do condition(:status_page_available) do
@subject.feature_available?(:status_page, @user) @subject.feature_available?(:status_page, @user)
end end
...@@ -185,6 +190,12 @@ module EE ...@@ -185,6 +190,12 @@ module EE
prevent :push_code prevent :push_code
end end
rule { analytics_disabled }.policy do
prevent(:read_project_merge_request_analytics)
prevent(:read_code_review_analytics)
prevent(:read_issue_analytics)
end
rule { feature_flags_related_issues_disabled | repository_disabled }.policy do rule { feature_flags_related_issues_disabled | repository_disabled }.policy do
prevent :admin_feature_flags_issue_links prevent :admin_feature_flags_issue_links
end end
...@@ -367,6 +378,8 @@ module EE ...@@ -367,6 +378,8 @@ module EE
prevent :modify_merge_request_committer_setting prevent :modify_merge_request_committer_setting
end end
rule { issue_analytics_enabled }.enable :read_issue_analytics
rule { can?(:read_merge_request) & code_review_analytics_enabled }.enable :read_code_review_analytics rule { can?(:read_merge_request) & code_review_analytics_enabled }.enable :read_code_review_analytics
rule { reporter & project_activity_analytics_available } rule { reporter & project_activity_analytics_available }
......
...@@ -3,6 +3,17 @@ ...@@ -3,6 +3,17 @@
module EE module EE
module Members module Members
module CreateService module CreateService
extend ::Gitlab::Utils::Override
override :execute
def execute(source)
if invite_quota_exceeded?(source, user_ids)
return error(s_("AddMember|Invite limit of %{daily_invites} per day exceeded") % { daily_invites: source.actual_limits.daily_invites })
end
super(source)
end
def after_execute(member:) def after_execute(member:)
super super
...@@ -18,6 +29,14 @@ module EE ...@@ -18,6 +29,14 @@ module EE
action: :create action: :create
).for_member(member).security_event ).for_member(member).security_event
end end
def invite_quota_exceeded?(source, user_ids)
return unless source.actual_limits.daily_invites
invite_count = ::Member.invite.created_today.in_hierarchy(source).count
source.actual_limits.exceeded?(:daily_invites, invite_count + user_ids.count)
end
end end
end end
end end
...@@ -134,8 +134,7 @@ module Gitlab ...@@ -134,8 +134,7 @@ module Gitlab
def base_headers def base_headers
@base_headers ||= { @base_headers ||= {
'Geo-GL-Id' => gl_id, 'Authorization' => Gitlab::Geo::BaseRequest.new(scope: auth_scope, gl_id: gl_id).authorization
'Authorization' => Gitlab::Geo::BaseRequest.new(scope: auth_scope).authorization
} }
end end
......
...@@ -14,6 +14,7 @@ RSpec.describe Gitlab::Geo::GitSSHProxy, :geo do ...@@ -14,6 +14,7 @@ RSpec.describe Gitlab::Geo::GitSSHProxy, :geo do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:user) { project.creator } let(:user) { project.creator }
let(:key) { create(:key, user: user) } let(:key) { create(:key, user: user) }
let(:key_identifier) { "key-#{key.id}" }
let(:base_request) { double(Gitlab::Geo::BaseRequest.new.authorization) } let(:base_request) { double(Gitlab::Geo::BaseRequest.new.authorization) }
let(:info_refs_body_short) do let(:info_refs_body_short) do
...@@ -22,7 +23,6 @@ RSpec.describe Gitlab::Geo::GitSSHProxy, :geo do ...@@ -22,7 +23,6 @@ RSpec.describe Gitlab::Geo::GitSSHProxy, :geo do
let(:base_headers) do let(:base_headers) do
{ {
'Geo-GL-Id' => "key-#{key.id}",
'Authorization' => 'secret' 'Authorization' => 'secret'
} }
end end
...@@ -72,13 +72,13 @@ RSpec.describe Gitlab::Geo::GitSSHProxy, :geo do ...@@ -72,13 +72,13 @@ RSpec.describe Gitlab::Geo::GitSSHProxy, :geo do
context 'authorization header is scoped' do context 'authorization header is scoped' do
it 'passes the scope when .info_refs_upload_pack is called' do it 'passes the scope when .info_refs_upload_pack is called' do
expect(Gitlab::Geo::BaseRequest).to receive(:new).with(scope: project.repository.full_path) expect(Gitlab::Geo::BaseRequest).to receive(:new).with(scope: project.repository.full_path, gl_id: key_identifier)
subject.info_refs_upload_pack subject.info_refs_upload_pack
end end
it 'passes the scope when .receive_pack is called' do it 'passes the scope when .receive_pack is called' do
expect(Gitlab::Geo::BaseRequest).to receive(:new).with(scope: project.repository.full_path) expect(Gitlab::Geo::BaseRequest).to receive(:new).with(scope: project.repository.full_path, gl_id: key_identifier)
subject.receive_pack(info_refs_body_short) subject.receive_pack(info_refs_body_short)
end end
...@@ -299,13 +299,13 @@ RSpec.describe Gitlab::Geo::GitSSHProxy, :geo do ...@@ -299,13 +299,13 @@ RSpec.describe Gitlab::Geo::GitSSHProxy, :geo do
context 'authorization header is scoped' do context 'authorization header is scoped' do
it 'passes the scope when .info_refs_receive_pack is called' do it 'passes the scope when .info_refs_receive_pack is called' do
expect(Gitlab::Geo::BaseRequest).to receive(:new).with(scope: project.repository.full_path) expect(Gitlab::Geo::BaseRequest).to receive(:new).with(scope: project.repository.full_path, gl_id: key_identifier )
subject.info_refs_receive_pack subject.info_refs_receive_pack
end end
it 'passes the scope when .receive_pack is called' do it 'passes the scope when .receive_pack is called' do
expect(Gitlab::Geo::BaseRequest).to receive(:new).with(scope: project.repository.full_path) expect(Gitlab::Geo::BaseRequest).to receive(:new).with(scope: project.repository.full_path, gl_id: key_identifier)
subject.receive_pack(info_refs_body_short) subject.receive_pack(info_refs_body_short)
end end
......
...@@ -10,58 +10,6 @@ RSpec.describe Geo::PushUser do ...@@ -10,58 +10,6 @@ RSpec.describe Geo::PushUser do
subject { described_class.new(gl_id) } subject { described_class.new(gl_id) }
describe '.needed_headers_provided?' do
where(:headers) do
[
{},
{ 'Geo-GL-Id' => nil },
{ 'Geo-GL-Id' => '' }
]
end
with_them do
it 'returns false' do
expect(described_class.needed_headers_provided?(headers)).to be(false)
end
end
context 'where gl_id is not nil' do
let(:headers) do
{ 'Geo-GL-Id' => gl_id }
end
it 'returns true' do
expect(described_class.needed_headers_provided?(headers)).to be(true)
end
end
end
describe '.new_from_headers' do
where(:headers) do
[
{},
{ 'Geo-GL-Id' => nil },
{ 'Geo-GL-Id' => '' }
]
end
with_them do
it 'returns false' do
expect(described_class.new_from_headers(headers)).to be_nil
end
end
context 'where gl_id is not nil' do
let(:headers) do
{ 'Geo-GL-Id' => gl_id }
end
it 'returns an instance of Geo::PushUser' do
expect(described_class.new_from_headers(headers)).to be_a(described_class)
end
end
end
describe '#user' do describe '#user' do
context 'with a junk gl_id' do context 'with a junk gl_id' do
let(:gl_id) { "test" } let(:gl_id) { "test" }
......
...@@ -1748,4 +1748,80 @@ RSpec.describe ProjectPolicy do ...@@ -1748,4 +1748,80 @@ RSpec.describe ProjectPolicy do
end end
end end
end end
describe 'read_analytics' do
context 'with various analytics features' do
let_it_be(:project_with_analytics_disabled) { create(:project, :analytics_disabled) }
let_it_be(:project_with_analytics_private) { create(:project, :analytics_private) }
let_it_be(:project_with_analytics_enabled) { create(:project, :analytics_enabled) }
before do
stub_licensed_features(issues_analytics: true, code_review_analytics: true, project_merge_request_analytics: true)
project_with_analytics_disabled.add_developer(developer)
project_with_analytics_private.add_developer(developer)
project_with_analytics_enabled.add_developer(developer)
end
context 'when analytics is enabled for the project' do
let(:project) { project_with_analytics_disabled }
context 'for guest user' do
let(:current_user) { guest }
it { is_expected.to be_disallowed(:read_project_merge_request_analytics) }
it { is_expected.to be_disallowed(:read_code_review_analytics) }
it { is_expected.to be_disallowed(:read_issue_analytics) }
end
context 'for developer' do
let(:current_user) { developer }
it { is_expected.to be_disallowed(:read_project_merge_request_analytics) }
it { is_expected.to be_disallowed(:read_code_review_analytics) }
it { is_expected.to be_disallowed(:read_issue_analytics) }
end
end
context 'when analytics is private for the project' do
let(:project) { project_with_analytics_private }
context 'for guest user' do
let(:current_user) { guest }
it { is_expected.to be_disallowed(:read_project_merge_request_analytics) }
it { is_expected.to be_disallowed(:read_code_review_analytics) }
it { is_expected.to be_disallowed(:read_issue_analytics) }
end
context 'for developer' do
let(:current_user) { developer }
it { is_expected.to be_allowed(:read_project_merge_request_analytics) }
it { is_expected.to be_allowed(:read_code_review_analytics) }
it { is_expected.to be_allowed(:read_issue_analytics) }
end
end
context 'when analytics is enabled for the project' do
let(:project) { project_with_analytics_private }
context 'for guest user' do
let(:current_user) { guest }
it { is_expected.to be_disallowed(:read_project_merge_request_analytics) }
it { is_expected.to be_disallowed(:read_code_review_analytics) }
it { is_expected.to be_disallowed(:read_issue_analytics) }
end
context 'for developer' do
let(:current_user) { developer }
it { is_expected.to be_allowed(:read_project_merge_request_analytics) }
it { is_expected.to be_allowed(:read_code_review_analytics) }
it { is_expected.to be_allowed(:read_issue_analytics) }
end
end
end
end
end end
...@@ -17,8 +17,6 @@ RSpec.describe "Git HTTP requests (Geo)", :geo do ...@@ -17,8 +17,6 @@ RSpec.describe "Git HTTP requests (Geo)", :geo do
let_it_be(:primary) { create(:geo_node, :primary, url: primary_url) } let_it_be(:primary) { create(:geo_node, :primary, url: primary_url) }
let_it_be(:secondary) { create(:geo_node, url: secondary_url) } let_it_be(:secondary) { create(:geo_node, url: secondary_url) }
# Ensure the token always comes from the real time of the request
let(:auth_token) { Gitlab::Geo::BaseRequest.new(scope: project.full_path).authorization }
let!(:user) { create(:user) } let!(:user) { create(:user) }
let!(:user_without_any_access) { create(:user) } let!(:user_without_any_access) { create(:user) }
let!(:user_without_push_access) { create(:user) } let!(:user_without_push_access) { create(:user) }
...@@ -131,6 +129,8 @@ RSpec.describe "Git HTTP requests (Geo)", :geo do ...@@ -131,6 +129,8 @@ RSpec.describe "Git HTTP requests (Geo)", :geo do
context 'when current node is a secondary' do context 'when current node is a secondary' do
let(:current_node) { secondary } let(:current_node) { secondary }
let(:auth_token) { Gitlab::Geo::BaseRequest.new(scope: project.full_path).authorization }
describe 'GET info_refs' do describe 'GET info_refs' do
context 'git pull' do context 'git pull' do
def make_request def make_request
...@@ -453,6 +453,10 @@ RSpec.describe "Git HTTP requests (Geo)", :geo do ...@@ -453,6 +453,10 @@ RSpec.describe "Git HTTP requests (Geo)", :geo do
end end
context 'when current node is the primary', :use_clean_rails_memory_store_caching do context 'when current node is the primary', :use_clean_rails_memory_store_caching do
let!(:geo_gl_id) { "key-#{key.id}" }
# Ensure the token always comes from the real time of the request
let(:auth_token) { Gitlab::Geo::BaseRequest.new(scope: project.full_path, gl_id: geo_gl_id).authorization }
let(:current_node) { primary } let(:current_node) { primary }
describe 'POST git_receive_pack' do describe 'POST git_receive_pack' do
...@@ -503,27 +507,7 @@ RSpec.describe "Git HTTP requests (Geo)", :geo do ...@@ -503,27 +507,7 @@ RSpec.describe "Git HTTP requests (Geo)", :geo do
let_it_be(:project) { project_with_repo } let_it_be(:project) { project_with_repo }
let(:endpoint_path) { "/#{project.full_path}.git/git-receive-pack" } let(:endpoint_path) { "/#{project.full_path}.git/git-receive-pack" }
before do context 'when gl_id is provided in JWT token' do
env['Geo-GL-Id'] = geo_gl_id
end
context 'when gl_id is incorrectly provided via HTTP headers' do
where(:geo_gl_id) do
[
nil,
''
]
end
with_them do
it 'returns a 403' do
is_expected.to have_gitlab_http_status(:forbidden)
expect(response.body).to eql('You are not allowed to upload code for this project.')
end
end
end
context 'when gl_id is provided via HTTP headers' do
context 'but is invalid' do context 'but is invalid' do
where(:geo_gl_id) do where(:geo_gl_id) do
%w[ %w[
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Members::CreateService do
let_it_be(:user) { create(:user) }
let_it_be(:root_ancestor) { create(:group) }
let_it_be(:project, reload: true) { create(:project, group: root_ancestor) }
let_it_be(:subgroup) { create(:group, parent: root_ancestor) }
let_it_be(:subgroup_project) { create(:project, group: subgroup) }
let_it_be(:project_users) { create_list(:user, 2) }
let(:params) { { user_ids: project_users.map(&:id).join(','), access_level: Gitlab::Access::GUEST } }
subject { described_class.new(user, params).execute(project) }
before_all do
project.add_maintainer(user)
create(:project_member, :invited, project: subgroup_project, created_at: 2.days.ago)
create(:project_member, :invited, project: subgroup_project)
create(:group_member, :invited, group: subgroup, created_at: 2.days.ago)
create(:group_member, :invited, group: subgroup)
end
context 'with group plan' do
let(:plan_limits) { create(:plan_limits, daily_invites: daily_invites) }
let(:plan) { create(:plan, limits: plan_limits) }
let!(:subscription) do
create(
:gitlab_subscription,
namespace: root_ancestor,
hosted_plan: plan
)
end
shared_examples 'quota limit exceeded' do |limit|
it { expect(subject).to include(status: :error, message: "Invite limit of #{limit} per day exceeded") }
it { expect { subject }.not_to change { Member.count } }
end
context 'already exceeded invite quota limit' do
let(:daily_invites) { 2 }
it_behaves_like 'quota limit exceeded', 2
end
context 'will exceed invite quota limit' do
let(:daily_invites) { 3 }
it_behaves_like 'quota limit exceeded', 3
end
context 'within invite quota limit' do
let(:daily_invites) { 5 }
it { expect(subject).to eq({ status: :success }) }
it do
subject
expect(project.users).to include(*project_users)
end
end
context 'infinite invite quota limit' do
let(:daily_invites) { 0 }
it { expect(subject).to eq({ status: :success }) }
it do
subject
expect(project.users).to include(*project_users)
end
end
end
context 'without a plan' do
let(:plan) { nil }
it { expect(subject).to eq({ status: :success }) }
it do
subject
expect(project.users).to include(*project_users)
end
end
end
...@@ -11,6 +11,8 @@ module API ...@@ -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
status 200 status 200
...@@ -55,7 +57,7 @@ module API ...@@ -55,7 +57,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 ...@@ -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 ...@@ -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 ...@@ -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
......
...@@ -248,6 +248,8 @@ module API ...@@ -248,6 +248,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,
...@@ -264,7 +266,10 @@ module API ...@@ -264,7 +266,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
...@@ -274,6 +279,8 @@ module API ...@@ -274,6 +279,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 =
...@@ -355,6 +362,8 @@ module API ...@@ -355,6 +362,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,
...@@ -370,6 +379,8 @@ module API ...@@ -370,6 +379,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 ...@@ -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
......
...@@ -61,8 +61,7 @@ module Gitlab ...@@ -61,8 +61,7 @@ module Gitlab
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}.merge(headers), }.merge(headers),
body: body, body: body
verify: false # FTC API Docs specifically mentions to turn off SSL Verification while making requests.
) )
end end
end end
......
...@@ -10,6 +10,10 @@ module Gitlab ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -82,22 +78,17 @@ module Gitlab
project.repository project.repository
end end
def entry_path(entry) # Ensure the path is in "path/" format
File.join(*[path, entry[:file_name]].compact).force_encoding(Encoding::ASCII_8BIT) def ensured_path
File.join(*[path, ""]) if path
end end
def build_entry(entry) def entry_path(entry)
{ file_name: entry.name, type: entry.type } File.join(*[path, entry[:file_name]].compact).force_encoding(Encoding::ASCII_8BIT)
end 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 ...@@ -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 ...@@ -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
......
...@@ -1923,6 +1923,9 @@ msgstr "" ...@@ -1923,6 +1923,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 ""
......
...@@ -56,18 +56,6 @@ RSpec.describe Projects::RefsController do ...@@ -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
...@@ -28,6 +28,7 @@ FactoryBot.define do ...@@ -28,6 +28,7 @@ FactoryBot.define do
forking_access_level { ProjectFeature::ENABLED } forking_access_level { ProjectFeature::ENABLED }
merge_requests_access_level { ProjectFeature::ENABLED } merge_requests_access_level { ProjectFeature::ENABLED }
repository_access_level { ProjectFeature::ENABLED } repository_access_level { ProjectFeature::ENABLED }
analytics_access_level { ProjectFeature::ENABLED }
pages_access_level do pages_access_level do
visibility_level == Gitlab::VisibilityLevel::PUBLIC ? ProjectFeature::ENABLED : ProjectFeature::PRIVATE visibility_level == Gitlab::VisibilityLevel::PUBLIC ? ProjectFeature::ENABLED : ProjectFeature::PRIVATE
end end
...@@ -63,7 +64,8 @@ FactoryBot.define do ...@@ -63,7 +64,8 @@ FactoryBot.define do
repository_access_level: evaluator.repository_access_level, repository_access_level: evaluator.repository_access_level,
pages_access_level: evaluator.pages_access_level, pages_access_level: evaluator.pages_access_level,
metrics_dashboard_access_level: evaluator.metrics_dashboard_access_level, metrics_dashboard_access_level: evaluator.metrics_dashboard_access_level,
operations_access_level: evaluator.operations_access_level operations_access_level: evaluator.operations_access_level,
analytics_access_level: evaluator.analytics_access_level
} }
project.build_project_feature(hash) project.build_project_feature(hash)
...@@ -335,6 +337,9 @@ FactoryBot.define do ...@@ -335,6 +337,9 @@ FactoryBot.define do
trait(:operations_enabled) { operations_access_level { ProjectFeature::ENABLED } } trait(:operations_enabled) { operations_access_level { ProjectFeature::ENABLED } }
trait(:operations_disabled) { operations_access_level { ProjectFeature::DISABLED } } trait(:operations_disabled) { operations_access_level { ProjectFeature::DISABLED } }
trait(:operations_private) { operations_access_level { ProjectFeature::PRIVATE } } trait(:operations_private) { operations_access_level { ProjectFeature::PRIVATE } }
trait(:analytics_enabled) { analytics_access_level { ProjectFeature::ENABLED } }
trait(:analytics_disabled) { analytics_access_level { ProjectFeature::DISABLED } }
trait(:analytics_private) { analytics_access_level { ProjectFeature::PRIVATE } }
trait :auto_devops do trait :auto_devops do
association :auto_devops, factory: :project_auto_devops association :auto_devops, factory: :project_auto_devops
......
...@@ -13,6 +13,8 @@ RSpec.describe Gitlab::Auth::Otp::Strategies::FortiTokenCloud do ...@@ -13,6 +13,8 @@ RSpec.describe Gitlab::Auth::Otp::Strategies::FortiTokenCloud do
let(:otp_verification_url) { url + '/auth' } let(:otp_verification_url) { url + '/auth' }
let(:access_token) { 'an_access_token' } let(:access_token) { 'an_access_token' }
let(:access_token_create_response_body) { '' } let(:access_token_create_response_body) { '' }
let(:access_token_request_body) { { client_id: client_id, client_secret: client_secret } }
let(:headers) { { 'Content-Type': 'application/json' } }
subject(:validate) { described_class.new(user).validate(otp_code) } subject(:validate) { described_class.new(user).validate(otp_code) }
...@@ -27,11 +29,8 @@ RSpec.describe Gitlab::Auth::Otp::Strategies::FortiTokenCloud do ...@@ -27,11 +29,8 @@ RSpec.describe Gitlab::Auth::Otp::Strategies::FortiTokenCloud do
client_secret: client_secret client_secret: client_secret
) )
access_token_request_body = { client_id: client_id,
client_secret: client_secret }
stub_request(:post, access_token_create_url) stub_request(:post, access_token_create_url)
.with(body: JSON(access_token_request_body), headers: { 'Content-Type' => 'application/json' }) .with(body: JSON(access_token_request_body), headers: headers)
.to_return( .to_return(
status: access_token_create_response_status, status: access_token_create_response_status,
body: Gitlab::Json.generate(access_token_create_response_body), body: Gitlab::Json.generate(access_token_create_response_body),
...@@ -81,6 +80,20 @@ RSpec.describe Gitlab::Auth::Otp::Strategies::FortiTokenCloud do ...@@ -81,6 +80,20 @@ RSpec.describe Gitlab::Auth::Otp::Strategies::FortiTokenCloud do
end end
end end
context 'SSL Verification' do
let(:access_token_create_response_status) { 400 }
context 'with `Gitlab::HTTP`' do
it 'does not use a `verify` argument,'\
'thereby always performing SSL verification while making API calls' do
expect(Gitlab::HTTP).to receive(:post)
.with(access_token_create_url, body: JSON(access_token_request_body), headers: headers).and_call_original
validate
end
end
end
def stub_forti_token_cloud_config(forti_token_cloud_settings) def stub_forti_token_cloud_config(forti_token_cloud_settings)
allow(::Gitlab.config.forti_token_cloud).to(receive_messages(forti_token_cloud_settings)) allow(::Gitlab.config.forti_token_cloud).to(receive_messages(forti_token_cloud_settings))
end end
......
...@@ -74,6 +74,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities 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 @@ ...@@ -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 ...@@ -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 ...@@ -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' }
......
# 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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 @@ ...@@ -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 ...@@ -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
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' }) 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
......
...@@ -1057,6 +1057,78 @@ RSpec.describe ProjectPolicy do ...@@ -1057,6 +1057,78 @@ RSpec.describe ProjectPolicy do
it { is_expected.to be_allowed(:read_analytics) } it { is_expected.to be_allowed(:read_analytics) }
end end
context 'with various analytics features' do
let_it_be(:project_with_analytics_disabled) { create(:project, :analytics_disabled) }
let_it_be(:project_with_analytics_private) { create(:project, :analytics_private) }
let_it_be(:project_with_analytics_enabled) { create(:project, :analytics_enabled) }
before do
project_with_analytics_disabled.add_developer(developer)
project_with_analytics_private.add_developer(developer)
project_with_analytics_enabled.add_developer(developer)
end
context 'when analytics is enabled for the project' do
let(:project) { project_with_analytics_disabled }
context 'for guest user' do
let(:current_user) { guest }
it { is_expected.to be_disallowed(:read_cycle_analytics) }
it { is_expected.to be_disallowed(:read_insights) }
it { is_expected.to be_disallowed(:read_repository_graphs) }
end
context 'for developer' do
let(:current_user) { developer }
it { is_expected.to be_disallowed(:read_cycle_analytics) }
it { is_expected.to be_disallowed(:read_insights) }
it { is_expected.to be_disallowed(:read_repository_graphs) }
end
end
context 'when analytics is private for the project' do
let(:project) { project_with_analytics_private }
context 'for guest user' do
let(:current_user) { guest }
it { is_expected.to be_disallowed(:read_cycle_analytics) }
it { is_expected.to be_disallowed(:read_insights) }
it { is_expected.to be_disallowed(:read_repository_graphs) }
end
context 'for developer' do
let(:current_user) { developer }
it { is_expected.to be_allowed(:read_cycle_analytics) }
it { is_expected.to be_allowed(:read_insights) }
it { is_expected.to be_allowed(:read_repository_graphs) }
end
end
context 'when analytics is enabled for the project' do
let(:project) { project_with_analytics_private }
context 'for guest user' do
let(:current_user) { guest }
it { is_expected.to be_disallowed(:read_cycle_analytics) }
it { is_expected.to be_disallowed(:read_insights) }
it { is_expected.to be_disallowed(:read_repository_graphs) }
end
context 'for developer' do
let(:current_user) { developer }
it { is_expected.to be_allowed(:read_cycle_analytics) }
it { is_expected.to be_allowed(:read_insights) }
it { is_expected.to be_allowed(:read_repository_graphs) }
end
end
end
context 'project member' do context 'project member' do
let(:project) { private_project } let(:project) { private_project }
......
...@@ -4,91 +4,136 @@ require 'spec_helper' ...@@ -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'))
end
it 'passes validation without warnings or errors' 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).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 end
it 'outputs expanded yaml content' do context 'when authenticated' do
post api('/ci/lint'), params: { content: yaml_content, include_merged_yaml: true } 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 have_key('merged_yaml') end
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 end
expect(json_response['status']).to eq('valid')
expect(json_response['errors']).to eq([]) context 'when authenticated' do
let_it_be(:api_user) { create(:user) }
it 'returns authentication success' do
post api('/ci/lint', api_user), params: { content: 'content' }
expect(response).to have_gitlab_http_status(:ok)
end
end 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 'responds with errors about invalid syntax' do context 'with valid .gitlab-ci.yaml content' do
post api('/ci/lint'), params: { content: yaml_content } 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(response).to have_gitlab_http_status(:ok)
expect(json_response['status']).to eq('invalid') 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"], invalid }' } 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 invalid config should implement a script: or a trigger: keyword', 'jobs config should contain at least one visible job']) expect(json_response['status']).to eq('valid')
expect(json_response['errors']).to eq([])
end end
end
it 'outputs expanded yaml content' do context 'with an invalid .gitlab_ci.yml' do
post api('/ci/lint'), params: { content: yaml_content, include_merged_yaml: true } context 'with invalid syntax' do
let(:yaml_content) { 'invalid content' }
expect(response).to have_gitlab_http_status(:ok) it 'responds with errors about invalid syntax' do
expect(json_response).to have_key('merged_yaml') 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
end end
end
context 'without the content parameter' do context 'without the content parameter' do
it 'responds with validation error about missing content' do it 'responds with validation error about missing content' do
post api('/ci/lint') post api('/ci/lint', api_user)
expect(response).to have_gitlab_http_status(:bad_request) expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('content is missing') 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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
...@@ -1226,6 +1226,12 @@ RSpec.describe API::MergeRequests do ...@@ -1226,6 +1226,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) }
...@@ -1411,6 +1417,12 @@ RSpec.describe API::MergeRequests do ...@@ -1411,6 +1417,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
...@@ -1436,6 +1448,12 @@ RSpec.describe API::MergeRequests do ...@@ -1436,6 +1448,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
...@@ -1511,6 +1529,12 @@ RSpec.describe API::MergeRequests do ...@@ -1511,6 +1529,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'
...@@ -1600,6 +1624,12 @@ RSpec.describe API::MergeRequests do ...@@ -1600,6 +1624,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
......
...@@ -886,6 +886,7 @@ RSpec.describe API::Projects do ...@@ -886,6 +886,7 @@ RSpec.describe API::Projects do
merge_method: 'ff' merge_method: 'ff'
}).tap do |attrs| }).tap do |attrs|
attrs[:operations_access_level] = 'disabled' attrs[:operations_access_level] = 'disabled'
attrs[:analytics_access_level] = 'disabled'
end end
post api('/projects', user), params: project post api('/projects', user), params: project
......
...@@ -331,6 +331,14 @@ RSpec.describe API::Todos do ...@@ -331,6 +331,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
......
# 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 ...@@ -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|
......
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment