Commit 6c2c0437 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch '197238-fix-isd-timeout' into 'master'

Fix query timeout on instance security dashboard

See merge request gitlab-org/gitlab!23232
parents 95296bae 7f86aa5a
...@@ -16,7 +16,10 @@ module Security ...@@ -16,7 +16,10 @@ module Security
end end
def vulnerable def vulnerable
@vulnerable ||= ApplicationInstance.new @vulnerable ||= InstanceSecurityDashboard.new(
current_user,
project_ids: params.fetch(:project_id, [])
)
end end
end end
end end
...@@ -3,37 +3,5 @@ ...@@ -3,37 +3,5 @@
module Security module Security
class VulnerabilityFindingsController < ::Security::ApplicationController class VulnerabilityFindingsController < ::Security::ApplicationController
include ProjectCollectionVulnerabilityFindingsActions include ProjectCollectionVulnerabilityFindingsActions
before_action :remove_invalid_project_ids
private
def remove_invalid_project_ids
render_empty_response if valid_project_ids.empty?
params[:project_id] = valid_project_ids
end
def render_empty_response
respond_to do |format|
format.json do
render json: {}
end
end
end
def valid_project_ids
return security_dashboard_project_ids if request_project_ids.empty?
security_dashboard_project_ids & request_project_ids
end
def request_project_ids
params.fetch(:project_id, []).map(&:to_i)
end
def security_dashboard_project_ids
current_user.security_dashboard_project_ids
end
end end
end end
...@@ -62,11 +62,11 @@ module EE ...@@ -62,11 +62,11 @@ module EE
end end
def security_dashboard_available? def security_dashboard_available?
app_instance = ApplicationInstance.new security_dashboard = InstanceSecurityDashboard.new(current_user)
::Feature.enabled?(:security_dashboard, default_enabled: true) && ::Feature.enabled?(:security_dashboard, default_enabled: true) &&
app_instance.feature_available?(:security_dashboard) && security_dashboard.feature_available?(:security_dashboard) &&
can?(current_user, :read_application_instance_security_dashboard, app_instance) can?(current_user, :read_instance_security_dashboard, security_dashboard)
end end
end end
end end
# frozen_string_literal: true
class ApplicationInstance
extend ActiveModel::Naming
include ::Vulnerable
def all_pipelines
::Ci::Pipeline.all
end
def feature_available?(feature)
License.feature_available?(feature)
end
end
...@@ -346,14 +346,6 @@ module EE ...@@ -346,14 +346,6 @@ module EE
read_attribute(:support_bot) read_attribute(:support_bot)
end end
def security_dashboard_project_ids
if self.can?(:read_all_resources)
security_dashboard_projects.ids
else
security_dashboard_projects.visible_to_user(self).ids
end
end
protected protected
override :password_required? override :password_required?
......
# frozen_string_literal: true
class InstanceSecurityDashboard
extend ActiveModel::Naming
include ::Vulnerable
def self.name
'Instance'
end
def initialize(user, project_ids: [])
@project_ids = project_ids
@user = user
end
def all_pipelines
::Ci::Pipeline.where(project_id: users_projects_with_security_reports)
end
def project_ids_with_security_reports
users_projects_with_security_reports.pluck(:project_id)
end
def feature_available?(feature)
License.feature_available?(feature)
end
private
attr_reader :project_ids, :user
def users_projects_with_security_reports
return visible_users_security_dashboard_projects if project_ids.empty?
visible_users_security_dashboard_projects.where(project_id: project_ids)
end
def visible_users_security_dashboard_projects
return users_security_dashboard_projects if user.can?(:read_all_resources)
users_security_dashboard_projects.where('EXISTS(?)', project_authorizations)
end
def users_security_dashboard_projects
UsersSecurityDashboardProject.select(:project_id).where(user: user)
end
def project_authorizations
ProjectAuthorization
.select(1)
.where(users_security_dashboard_projects: { user_id: user.id })
.where(project_authorizations: { user_id: user.id })
.where('users_security_dashboard_projects.project_id = project_authorizations.project_id')
end
end
# frozen_string_literal: true
class ApplicationInstancePolicy < BasePolicy
rule { ~anonymous }.enable :read_application_instance_security_dashboard
end
# frozen_string_literal: true
class InstancePolicy < BasePolicy
rule { ~anonymous }.enable :read_instance_security_dashboard
end
---
title: Fix vulnerability finding list endpoint query timeout on instance security dashboard
merge_request: 23232
author:
type: fixed
...@@ -58,10 +58,6 @@ module Gitlab ...@@ -58,10 +58,6 @@ module Gitlab
return filters[:project_id] if filters.key?('project_id') return filters[:project_id] if filters.key?('project_id')
vulnerable.project_ids_with_security_reports vulnerable.project_ids_with_security_reports
rescue NoMethodError
vulnerable_name = vulnerable.model_name.human.downcase
raise NoProjectIDsError, "A project_id filter must be given with this #{vulnerable_name}"
end end
end end
end end
......
...@@ -60,11 +60,13 @@ module Gitlab ...@@ -60,11 +60,13 @@ module Gitlab
end end
def project_ids_to_fetch def project_ids_to_fetch
project_ids = vulnerable.is_a?(Project) ? [vulnerable.id] : [] return [vulnerable.id] if vulnerable.is_a?(Project)
return filters[:project_id] + project_ids if filters.key?('project_id') if filters.key?('project_id')
vulnerable.project_ids_with_security_reports & filters[:project_id].map(&:to_i)
vulnerable.is_a?(Group) ? vulnerable.project_ids_with_security_reports : project_ids else
vulnerable.project_ids_with_security_reports
end
end end
end end
end end
......
...@@ -60,7 +60,7 @@ describe DashboardHelper, type: :helper do ...@@ -60,7 +60,7 @@ describe DashboardHelper, type: :helper do
where(:ability, :feature_flag, :nav_link) do where(:ability, :feature_flag, :nav_link) do
:read_operations_dashboard | nil | :operations :read_operations_dashboard | nil | :operations
:read_operations_dashboard | :environments_dashboard | :environments :read_operations_dashboard | :environments_dashboard | :environments
:read_application_instance_security_dashboard | :security_dashboard | :security :read_instance_security_dashboard | :security_dashboard | :security
end end
with_them do with_them do
...@@ -120,10 +120,9 @@ describe DashboardHelper, type: :helper do ...@@ -120,10 +120,9 @@ describe DashboardHelper, type: :helper do
def stub_resource_visibility(feature_flag, read_other_resources:, read_security_dashboard:, security_dashboard_available:) def stub_resource_visibility(feature_flag, read_other_resources:, read_security_dashboard:, security_dashboard_available:)
if feature_flag == :security_dashboard if feature_flag == :security_dashboard
app_instance = double(ApplicationInstance, feature_available?: security_dashboard_available) stub_licensed_features(feature_flag => security_dashboard_available)
allow(ApplicationInstance).to receive(:new).and_return(app_instance)
allow(helper).to receive(:can?).with(user, ability, app_instance).and_return(read_security_dashboard) allow(helper).to receive(:can?).and_return(read_security_dashboard)
else else
allow(helper).to receive(:can?).with(user, ability).and_return(read_other_resources) allow(helper).to receive(:can?).with(user, ability).and_return(read_other_resources)
end end
......
...@@ -55,10 +55,16 @@ describe Gitlab::Vulnerabilities::HistoryCache do ...@@ -55,10 +55,16 @@ describe Gitlab::Vulnerabilities::HistoryCache do
end end
end end
context 'when given an ApplicationInstance' do context 'when given an InstanceSecurityDashboard' do
it_behaves_like 'the history cache when given an expected Vulnerable' do it_behaves_like 'the history cache when given an expected Vulnerable' do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:vulnerable) { ApplicationInstance.new } let(:user) { create(:user) }
let(:vulnerable) { InstanceSecurityDashboard.new(user) }
before do
project.add_developer(user)
user.security_dashboard_projects << project
end
end end
end end
end end
......
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
describe Gitlab::Vulnerabilities::History do describe Gitlab::Vulnerabilities::History do
describe '#findings_counter', :use_clean_rails_memory_store_caching do describe '#findings_counter', :use_clean_rails_memory_store_caching do
shared_examples 'the history cache when given an expected Vulnerable' do shared_examples 'the history cache when given an expected Vulnerable' do
let(:filters) { project_ids } let(:filters) { ActionController::Parameters.new }
let(:today) { Date.parse('20191031') } let(:today) { Date.parse('20191031') }
before do before do
...@@ -18,7 +18,7 @@ describe Gitlab::Vulnerabilities::History do ...@@ -18,7 +18,7 @@ describe Gitlab::Vulnerabilities::History do
subject(:counter) { described_class.new(vulnerable, params: filters).findings_counter } subject(:counter) { described_class.new(vulnerable, params: filters).findings_counter }
context 'when filters are passed' do context 'when filters are passed' do
let(:filters) { project_ids.merge(report_type: :sast) } let(:filters) { ActionController::Parameters.new({ 'report_type' => ['sast'] }) }
it 'does not call Gitlab::Vulnerabilities::HistoryCache' do it 'does not call Gitlab::Vulnerabilities::HistoryCache' do
expect(Gitlab::Vulnerabilities::HistoryCache).not_to receive(:new) expect(Gitlab::Vulnerabilities::HistoryCache).not_to receive(:new)
...@@ -57,6 +57,20 @@ describe Gitlab::Vulnerabilities::History do ...@@ -57,6 +57,20 @@ describe Gitlab::Vulnerabilities::History do
end end
end end
context 'when a project_id filter is passed' do
let(:filters) { ActionController::Parameters.new({ 'project_id' => [project1] }) }
it 'only fetches history for the filtered by projects' do
expect(Gitlab::Vulnerabilities::HistoryCache).to receive(:new).once.and_call_original
Timecop.freeze(today) do
expect(counter[:total]).to eq({ today => 1 })
expect(counter[:high]).to eq({})
expect(counter[:medium]).to eq({ today => 1 })
end
end
end
def create_vulnerabilities(count, project, options = {}) def create_vulnerabilities(count, project, options = {})
report_type = options[:report_type] || :sast report_type = options[:report_type] || :sast
severity = options[:severity] || :high severity = options[:severity] || :high
...@@ -71,29 +85,23 @@ describe Gitlab::Vulnerabilities::History do ...@@ -71,29 +85,23 @@ describe Gitlab::Vulnerabilities::History do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:project1) { create(:project, :public, namespace: group) } let(:project1) { create(:project, :public, namespace: group) }
let(:project2) { create(:project, :public, namespace: group) } let(:project2) { create(:project, :public, namespace: group) }
let(:project_ids) { {} }
let(:vulnerable) { group } let(:vulnerable) { group }
end end
end end
context 'when given an ApplicationInstance' do context 'when given an InstanceSecurityDashboard' do
let(:vulnerable) { ApplicationInstance.new }
context 'and a project_id filter' do
it_behaves_like 'the history cache when given an expected Vulnerable' do it_behaves_like 'the history cache when given an expected Vulnerable' do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:project1) { create(:project, :public, namespace: group) } let(:project1) { create(:project, :public, namespace: group) }
let(:project2) { create(:project, :public, namespace: group) } let(:project2) { create(:project, :public, namespace: group) }
let(:project_ids) { ActionController::Parameters.new({ 'project_id' => [project1, project2] }) } let(:user) { create(:user) }
end let(:vulnerable) { InstanceSecurityDashboard.new(user) }
end
before do
project1.add_developer(user)
project2.add_developer(user)
context 'and no project_id filter' do user.security_dashboard_projects << [project1, project2]
it 'throws an error saying that the filter must be given' do
expect { described_class.new(vulnerable, params: {}).findings_counter }.to raise_error(
Gitlab::Vulnerabilities::History::NoProjectIDsError,
"A project_id filter must be given with this #{vulnerable.model_name.human.downcase}"
)
end end
end end
end end
......
...@@ -60,6 +60,15 @@ describe Gitlab::Vulnerabilities::Summary do ...@@ -60,6 +60,15 @@ describe Gitlab::Vulnerabilities::Summary do
expect(counter[:high]).to eq(2) expect(counter[:high]).to eq(2)
end end
end end
context 'when a project_id param is passed' do
let(:filters) { ActionController::Parameters.new({ project_id: [project1.id.to_s] }) }
it 'only fetches findings for the given projects' do
expect(counter[:high]).to eq(0)
expect(counter[:medium]).to eq(1)
end
end
end end
def create_vulnerabilities(count, project, options = {}) def create_vulnerabilities(count, project, options = {})
......
# frozen_string_literal: true
require 'spec_helper'
describe ApplicationInstance do
it_behaves_like Vulnerable do
let(:vulnerable) { described_class.new }
end
describe '#all_pipelines' do
it 'returns all CI pipelines for the instance' do
allow(::Ci::Pipeline).to receive(:all)
described_class.new.all_pipelines
expect(::Ci::Pipeline).to have_received(:all)
end
end
describe '#feature_available?' do
subject { described_class.new.feature_available?(:security_dashboard) }
context "when the feature is available for the instance's license" do
before do
stub_licensed_features(security_dashboard: true)
end
it 'returns true' do
is_expected.to be_truthy
end
end
context "when the feature is not available for the instance's license" do
before do
stub_licensed_features(security_dashboard: false)
end
it 'returns false' do
is_expected.to be_falsy
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe InstanceSecurityDashboard do
let(:pipeline1) { create(:ci_pipeline, project: project1) }
let(:pipeline2) { create(:ci_pipeline, project: project2) }
let(:project1) { create(:project) }
let(:project2) { create(:project) }
let(:project_ids) { [project1.id] }
let(:user) { create(:user) }
before do
project1.add_developer(user)
user.security_dashboard_projects << [project1, project2]
end
subject { described_class.new(user, project_ids: project_ids) }
it_behaves_like Vulnerable do
let(:vulnerable) { described_class.new(user, project_ids: project_ids) }
end
describe '.name' do
it 'is programmatically named Instance' do
expect(described_class.name).to eq('Instance')
end
end
describe '#all_pipelines' do
it 'returns pipelines for the projects with security reports' do
expect(subject.all_pipelines).to contain_exactly(pipeline1)
end
end
describe '#project_ids_with_security_reports' do
context 'when given project IDs' do
it "returns the project IDs that are also on the user's security dashboard" do
expect(subject.project_ids_with_security_reports).to contain_exactly(project1.id)
end
end
context 'when not given project IDs' do
let(:project_ids) { [] }
it "returns the security dashboard projects' IDs" do
expect(subject.project_ids_with_security_reports).to contain_exactly(project1.id)
end
end
context 'when the user cannot read all resources' do
let(:project_ids) { [project1.id, project2.id] }
it 'only includes projects they can read' do
expect(subject.project_ids_with_security_reports).to contain_exactly(project1.id)
end
end
context 'when the user can read all resources' do
let(:project_ids) { [project1.id, project2.id] }
let(:user) { create(:auditor) }
it 'includes all dashboard projects' do
expect(subject.project_ids_with_security_reports).to contain_exactly(project1.id, project2.id)
end
end
end
describe '#feature_available?' do
subject { described_class.new(user).feature_available?(:security_dashboard) }
context "when the feature is available for the instance's license" do
before do
stub_licensed_features(security_dashboard: true)
end
it 'returns true' do
is_expected.to be_truthy
end
end
context "when the feature is not available for the instance's license" do
before do
stub_licensed_features(security_dashboard: false)
end
it 'returns false' do
is_expected.to be_falsy
end
end
end
end
...@@ -836,32 +836,4 @@ describe User do ...@@ -836,32 +836,4 @@ describe User do
end end
end end
end end
describe '#security_dashboard_project_ids' do
let(:project) { create(:project) }
context 'when the user can read all resources' do
it "returns the ids for all of the user's security dashboard projects" do
admin = create(:admin)
auditor = create(:auditor)
admin.security_dashboard_projects << project
auditor.security_dashboard_projects << project
expect(admin.security_dashboard_project_ids).to eq([project.id])
expect(auditor.security_dashboard_project_ids).to eq([project.id])
end
end
context 'when the user cannot read all resources' do
it 'returns the ids for security dashboard projects visible to the user' do
user = create(:user)
member_project = create(:project)
member_project.add_developer(user)
user.security_dashboard_projects << [project, member_project]
expect(user.security_dashboard_project_ids).to eq([member_project.id])
end
end
end
end end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe ApplicationInstancePolicy do describe InstancePolicy do
let(:current_user) { create(:user) } let(:current_user) { create(:user) }
let(:user) { create(:user) } let(:user) { create(:user) }
...@@ -12,15 +12,15 @@ describe ApplicationInstancePolicy do ...@@ -12,15 +12,15 @@ describe ApplicationInstancePolicy do
subject { described_class.new(current_user, [user]) } subject { described_class.new(current_user, [user]) }
describe 'read_application_instance_security_dashboard' do describe 'read_instance_security_dashboard' do
context 'when the user is not logged in' do context 'when the user is not logged in' do
let(:current_user) { nil } let(:current_user) { nil }
it { is_expected.not_to be_allowed(:read_application_instance_security_dashboard) } it { is_expected.not_to be_allowed(:read_instance_security_dashboard) }
end end
context 'when the user is logged in' do context 'when the user is logged in' do
it { is_expected.to be_allowed(:read_application_instance_security_dashboard) } it { is_expected.to be_allowed(:read_instance_security_dashboard) }
end end
end end
end end
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module VulnerableHelpers module VulnerableHelpers
class BadVulnerableError < StandardError class BadVulnerableError < StandardError
def message def message
'The given vulnerable must be either `Project`, `Namespace`, or `ApplicationInstance`' 'The given vulnerable must be either `Project`, `Namespace`, or `InstanceSecurityDashboard`'
end end
end end
...@@ -13,21 +13,8 @@ module VulnerableHelpers ...@@ -13,21 +13,8 @@ module VulnerableHelpers
vulnerable vulnerable
when Namespace when Namespace
create(:project, namespace: vulnerable) create(:project, namespace: vulnerable)
when ApplicationInstance when InstanceSecurityDashboard
create(:project) Project.find(vulnerable.project_ids_with_security_reports.first)
else
raise BadVulnerableError
end
end
def as_external_vulnerable_project(vulnerable)
case vulnerable
when Project
create(:project)
when Namespace
create(:project)
when ApplicationInstance
nil
else else
raise BadVulnerableError raise BadVulnerableError
end end
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
RSpec.shared_examples Vulnerable do RSpec.shared_examples Vulnerable do
include VulnerableHelpers include VulnerableHelpers
let(:external_project) { as_external_vulnerable_project(vulnerable) } let(:external_project) { create(:project) }
let(:failed_pipeline) { create(:ci_pipeline, :failed, project: vulnerable_project) } let(:failed_pipeline) { create(:ci_pipeline, :failed, project: vulnerable_project) }
let!(:old_vuln) { create_vulnerability(vulnerable_project) } let!(:old_vuln) { create_vulnerability(vulnerable_project) }
......
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