Commit 63f0bc09 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/security/gitlab@13-8-stable-ee

parent efdc7889
...@@ -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') }
......
# 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)
......
---
title: Cancel running and pending jobs when a project is deleted
merge_request: 1220
author:
type: security
---
title: Limit daily invitations to groups and projects
merge_request:
author:
type: security
---
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
...@@ -15254,7 +15254,8 @@ CREATE TABLE plan_limits ( ...@@ -15254,7 +15254,8 @@ CREATE TABLE plan_limits (
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,
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
); );
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 ...@@ -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.
......
...@@ -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
......
...@@ -1807,6 +1807,9 @@ msgstr "" ...@@ -1807,6 +1807,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 ""
......
...@@ -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
......
# 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
......
# 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|
......
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
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