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

Merge branch 'pl-status-page-mvc-delete-confidential' into 'master'

Unpublish details for confidential issues

See merge request gitlab-org/gitlab!27263
parents 29fe14e2 de0b84a8
...@@ -234,7 +234,7 @@ ...@@ -234,7 +234,7 @@
- 2 - 2
- - service_desk_email_receiver - - service_desk_email_receiver
- 1 - 1
- - status_page_publish_incident - - status_page_publish
- 1 - 1
- - sync_seat_link_request - - sync_seat_link_request
- 1 - 1
......
...@@ -10,10 +10,12 @@ ...@@ -10,10 +10,12 @@
# #
# finder = StatusPage::IncidentsFinder.new(project_id: project_id) # finder = StatusPage::IncidentsFinder.new(project_id: project_id)
# #
# # A single issue # # A single issue which includes confidential issues by default)
# issue, user_notes = finder.find_by_id(issue_id) # issue = finder.find_by_id(issue_id)
# # Find a "public only" issue
# issue = finder.find_by_id(issue_id, include_confidential: false)
# #
# # Most recent 20 issues # # Most recent 20 non-confidential issues
# issues = finder.all # issues = finder.all
# #
module StatusPage module StatusPage
...@@ -24,24 +26,23 @@ module StatusPage ...@@ -24,24 +26,23 @@ module StatusPage
@project_id = project_id @project_id = project_id
end end
def find_by_id(issue_id) def find_by_id(issue_id, include_confidential: true)
execute.find_by_id(issue_id) execute(include_confidential: include_confidential)
.find_by_id(issue_id)
end end
def all def all
@sorted = true execute(sorted: true)
execute
.limit(MAX_LIMIT) # rubocop: disable CodeReuse/ActiveRecord .limit(MAX_LIMIT) # rubocop: disable CodeReuse/ActiveRecord
end end
private private
attr_reader :project_id, :with_user_notes, :sorted attr_reader :project_id
def execute def execute(sorted: false, include_confidential: false)
issues = init_collection issues = init_collection
issues = public_only(issues) issues = public_only(issues) unless include_confidential
issues = by_project(issues) issues = by_project(issues)
issues = reverse_chronological(issues) if sorted issues = reverse_chronological(issues) if sorted
issues issues
......
...@@ -12,14 +12,14 @@ module StatusPage ...@@ -12,14 +12,14 @@ module StatusPage
return error_feature_not_available unless feature_available? return error_feature_not_available unless feature_available?
return error_no_storage_client unless storage_client return error_no_storage_client unless storage_client
publish(*args) process(*args)
end end
private private
attr_reader :project attr_reader :project
def publish(*args) def process(*args)
raise NotImplementedError raise NotImplementedError
end end
...@@ -53,6 +53,12 @@ module StatusPage ...@@ -53,6 +53,12 @@ module StatusPage
success(object_key: key) success(object_key: key)
end end
def delete(key)
storage_client.delete_object(key)
success(object_key: key)
end
def limit_exceeded?(json) def limit_exceeded?(json)
!Gitlab::Utils::DeepSize !Gitlab::Utils::DeepSize
.new(json, max_size: Storage::JSON_MAX_SIZE) .new(json, max_size: Storage::JSON_MAX_SIZE)
......
...@@ -4,13 +4,13 @@ module StatusPage ...@@ -4,13 +4,13 @@ module StatusPage
# Render an issue as incident details and publish them to CDN. # Render an issue as incident details and publish them to CDN.
# #
# This is an internal service which is part of # This is an internal service which is part of
# +StatusPage::PublishIncidentService+ and is not meant to be called directly. # +StatusPage::PublishService+ and is not meant to be called directly.
# #
# Consider calling +StatusPage::PublishIncidentService+ instead. # Consider calling +StatusPage::PublishService+ instead.
class PublishDetailsService < PublishBaseService class PublishDetailsService < PublishBaseService
private private
def publish(issue, user_notes) def process(issue, user_notes)
json = serialize(issue, user_notes) json = serialize(issue, user_notes)
key = object_key(json) key = object_key(json)
return error('Missing object key') unless key return error('Missing object key') unless key
......
...@@ -4,13 +4,13 @@ module StatusPage ...@@ -4,13 +4,13 @@ module StatusPage
# Render a list of issues as incidents and publish them to CDN. # Render a list of issues as incidents and publish them to CDN.
# #
# This is an internal service which is part of # This is an internal service which is part of
# +StatusPage::PublishIncidentService+ and is not meant to be called directly. # +StatusPage::PublishService+ and is not meant to be called directly.
# #
# Consider calling +StatusPage::PublishIncidentService+ instead. # Consider calling +StatusPage::PublishService+ instead.
class PublishListService < PublishBaseService class PublishListService < PublishBaseService
private private
def publish(issues) def process(issues)
json = serialize(issues) json = serialize(issues)
upload(object_key, json) upload(object_key, json)
......
...@@ -9,8 +9,9 @@ module StatusPage ...@@ -9,8 +9,9 @@ module StatusPage
# #
# This services calls: # This services calls:
# * StatusPage::PublishDetailsService # * StatusPage::PublishDetailsService
# * StatusPage::UnpublishDetailsService
# * StatusPage::PublishListService # * StatusPage::PublishListService
class PublishIncidentService class PublishService
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
def initialize(user:, project:, issue_id:) def initialize(user:, project:, issue_id:)
...@@ -23,22 +24,34 @@ module StatusPage ...@@ -23,22 +24,34 @@ module StatusPage
return error_permission_denied unless can_publish? return error_permission_denied unless can_publish?
return error_issue_not_found unless issue return error_issue_not_found unless issue
response = publish_details response = process_details
return response if response.error? return response if response.error?
publish_list process_list
end end
private private
attr_reader :user, :project, :issue_id attr_reader :user, :project, :issue_id
def process_details
if issue.confidential?
unpublish_details
else
publish_details
end
end
def process_list
PublishListService.new(project: project).execute(issues)
end
def publish_details def publish_details
PublishDetailsService.new(project: project).execute(issue, user_notes) PublishDetailsService.new(project: project).execute(issue, user_notes)
end end
def publish_list def unpublish_details
PublishListService.new(project: project).execute(issues) UnpublishDetailsService.new(project: project).execute(issue)
end end
def issue def issue
......
...@@ -15,8 +15,7 @@ module StatusPage ...@@ -15,8 +15,7 @@ module StatusPage
return unless can_publish? return unless can_publish?
return unless status_page_enabled? return unless status_page_enabled?
StatusPage::PublishIncidentWorker StatusPage::PublishWorker.perform_async(user.id, project.id, issue_id)
.perform_async(user.id, project.id, issue_id)
end end
private private
......
# frozen_string_literal: true
module StatusPage
# Unpublish incident details from CDN.
#
# Example: An issue becomes confidential so it must be removed from CDN.
#
# This is an internal service which is part of
# +StatusPage::PublishService+ and is not meant to be called directly.
#
# Consider calling +StatusPage::PublishService+ instead.
class UnpublishDetailsService < PublishBaseService
private
def process(issue)
key = object_key(issue)
delete(key)
end
def object_key(issue)
StatusPage::Storage.details_path(issue.iid)
end
end
end
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
:idempotent: true :idempotent: true
- :name: cronjob:elastic_metrics_update - :name: cronjob:elastic_metrics_update
:feature_category: :global_search :feature_category: :global_search
:has_external_dependencies: :has_external_dependencies:
:urgency: :low :urgency: :low
:resource_boundary: :unknown :resource_boundary: :unknown
:weight: 1 :weight: 1
...@@ -577,7 +577,7 @@ ...@@ -577,7 +577,7 @@
:resource_boundary: :unknown :resource_boundary: :unknown
:weight: 1 :weight: 1
:idempotent: :idempotent:
- :name: status_page_publish_incident - :name: status_page_publish
:feature_category: :status_page :feature_category: :status_page
:has_external_dependencies: true :has_external_dependencies: true
:urgency: :low :urgency: :low
......
# frozen_string_literal: true # frozen_string_literal: true
module StatusPage module StatusPage
class PublishIncidentWorker class PublishWorker
include ApplicationWorker include ApplicationWorker
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
...@@ -26,7 +26,7 @@ module StatusPage ...@@ -26,7 +26,7 @@ module StatusPage
attr_reader :user_id, :project_id, :issue_id attr_reader :user_id, :project_id, :issue_id
def publish def publish
result = PublishIncidentService result = PublishService
.new(user: user, project: project, issue_id: issue_id) .new(user: user, project: project, issue_id: issue_id)
.execute .execute
......
...@@ -25,6 +25,17 @@ module StatusPage ...@@ -25,6 +25,17 @@ module StatusPage
true true
end end
# Deletes +key+ from storage
#
# Note, this operation succeeds even if +key+ does not exist in storage.
def delete_object(key)
wrap_errors(key: key) do
client.delete_object(bucket: bucket_name, key: key)
end
true
end
private private
attr_reader :client, :bucket_name attr_reader :client, :bucket_name
......
...@@ -17,29 +17,44 @@ describe StatusPage::IncidentsFinder do ...@@ -17,29 +17,44 @@ describe StatusPage::IncidentsFinder do
let(:finder) { described_class.new(project_id: project.id) } let(:finder) { described_class.new(project_id: project.id) }
describe '#find_by_id' do describe '#find_by_id' do
subject { finder.find_by_id(issue.id) } subject { finder.find_by_id(issue.id, **params) }
context 'for public issue' do context 'without params' do
let(:issue) { public_issues.first } let(:params) { {} }
it { is_expected.to eq(issue) } context 'for public issue' do
end let(:issue) { public_issues.first }
it { is_expected.to eq(issue) }
end
context 'for confidential issue' do
let(:issue) { issues.fetch(:confidential) }
context 'for confidential issue' do it { is_expected.to eq(issue) }
let(:issue) { issues.fetch(:confidential) } end
it { is_expected.to be_nil } context 'for unrelated issue' do
let(:issue) { issues.fetch(:unrelated) }
it { is_expected.to be_nil }
end
end end
context 'for unrelated issue' do context 'with include_confidential' do
let(:issue) { issues.fetch(:unrelated) } let(:params) { { include_confidential: false } }
context 'for confidential issue' do
let(:issue) { issues.fetch(:confidential) }
it { is_expected.to be_nil } it { is_expected.to be_nil }
end
end end
end end
describe '#all' do describe '#all' do
let(:sorted_issues) { public_issues.sort_by(&:created_at).reverse } let(:sorted_issues) { public_issues.sort_by(&:created_at).reverse }
let(:limit) { public_issues.size }
subject { finder.all } subject { finder.all }
...@@ -58,5 +73,13 @@ describe StatusPage::IncidentsFinder do ...@@ -58,5 +73,13 @@ describe StatusPage::IncidentsFinder do
it { is_expected.to eq(sorted_issues.first(1)) } it { is_expected.to eq(sorted_issues.first(1)) }
end end
context 'when combined with other finder methods' do
before do
finder.find_by_id(public_issues.first.id)
end
it { is_expected.to eq(sorted_issues) }
end
end end
end end
...@@ -41,6 +41,29 @@ describe StatusPage::Storage::S3Client, :aws_s3 do ...@@ -41,6 +41,29 @@ describe StatusPage::Storage::S3Client, :aws_s3 do
end end
end end
describe '#delete_object' do
let(:key) { 'key' }
subject(:result) { client.delete_object(key) }
it 'returns true' do
stub_responses(:delete_object)
expect(result).to eq(true)
end
context 'when failed' do
let(:aws_error) { 'SomeError' }
it 'raises an error' do
stub_responses(:delete_object, aws_error)
msg = error_message(aws_error, key: key)
expect { result }.to raise_error(StatusPage::Storage::Error, msg)
end
end
end
private private
def stub_responses(*args) def stub_responses(*args)
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe StatusPage::PublishIncidentService do describe StatusPage::PublishService do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:project, refind: true) { create(:project) } let_it_be(:project, refind: true) { create(:project) }
let_it_be(:issue) { create(:issue, project: project) } let_it_be(:issue) { create(:issue, project: project) }
...@@ -23,29 +23,54 @@ describe StatusPage::PublishIncidentService do ...@@ -23,29 +23,54 @@ describe StatusPage::PublishIncidentService do
.and_return(user_can_publish) .and_return(user_can_publish)
end end
context 'when publishing succeeds' do describe 'publish details' do
it 'returns uploads incidents details and list' do context 'when upload succeeds' do
expect_to_upload_details(issue) it 'uploads incident details and list' do
expect_to_upload_list expect_to_upload_details(issue)
expect_to_upload_list
expect(result).to be_success expect(result).to be_success
end
end
context 'when upload fails' do
it 'propagates the exception' do
expect_to_upload_details(issue, status: 404)
expect { result }.to raise_error(StatusPage::Storage::Error)
end
end end
end end
context 'when uploading details fails' do describe 'unpublish details' do
it 'propagates the exception' do let_it_be(:issue) { create(:issue, :confidential, project: project) }
expect_to_upload_details(issue, status: 404)
expect { result }.to raise_error(StatusPage::Storage::Error) context 'when deletion succeeds' do
it 'deletes incident details and upload list' do
expect_to_delete_details(issue)
expect_to_upload_list
expect(result).to be_success
end
end
context 'when deletion fails' do
it 'propagates the exception' do
expect_to_delete_details(issue, status: 403)
expect { result }.to raise_error(StatusPage::Storage::Error)
end
end end
end end
context 'when uploading list fails' do describe 'publish list' do
it 'returns error and skip list upload' do context 'when upload fails' do
expect_to_upload_details(issue) it 'returns error and skip list upload' do
expect_to_upload_list(status: 404) expect_to_upload_details(issue)
expect_to_upload_list(status: 404)
expect { result }.to raise_error(StatusPage::Storage::Error) expect { result }.to raise_error(StatusPage::Storage::Error)
end
end end
end end
...@@ -71,14 +96,18 @@ describe StatusPage::PublishIncidentService do ...@@ -71,14 +96,18 @@ describe StatusPage::PublishIncidentService do
private private
def expect_to_upload_details(issue, **kwargs) def expect_to_upload_details(issue, **kwargs)
stub_upload_request(StatusPage::Storage.details_path(issue.iid), **kwargs) stub_aws_request(:put, StatusPage::Storage.details_path(issue.iid), **kwargs)
end
def expect_to_delete_details(issue, **kwargs)
stub_aws_request(:delete, StatusPage::Storage.details_path(issue.iid), **kwargs)
end end
def expect_to_upload_list(**kwargs) def expect_to_upload_list(**kwargs)
stub_upload_request(StatusPage::Storage.list_path, **kwargs) stub_aws_request(:put, StatusPage::Storage.list_path, **kwargs)
end end
def stub_upload_request(path, status: 200) def stub_aws_request(method, path, status: 200)
stub_request(:put, %r{amazonaws.com/#{path}}).to_return(status: status) stub_request(method, %r{amazonaws.com/#{path}}).to_return(status: status)
end end
end end
...@@ -7,7 +7,7 @@ describe StatusPage::TriggerPublishService do ...@@ -7,7 +7,7 @@ describe StatusPage::TriggerPublishService do
let_it_be(:project, refind: true) { create(:project) } let_it_be(:project, refind: true) { create(:project) }
let_it_be(:issue) { create(:issue, project: project) } let_it_be(:issue) { create(:issue, project: project) }
let(:service) { described_class.new(user: user, project: project) } let(:service) { described_class.new(user: user, project: project) }
let(:worker) { StatusPage::PublishIncidentWorker } let(:worker) { StatusPage::PublishWorker }
let_it_be(:status_page_setting) do let_it_be(:status_page_setting) do
create(:status_page_setting, :enabled, project: project) create(:status_page_setting, :enabled, project: project)
......
# frozen_string_literal: true
require 'spec_helper'
describe StatusPage::UnpublishDetailsService do
let_it_be(:project, refind: true) { create(:project) }
let(:issue) { instance_double(Issue, iid: incident_id) }
let(:incident_id) { 1 }
let(:key) { StatusPage::Storage.details_path(incident_id) }
let(:service) { described_class.new(project: project) }
subject(:result) { service.execute(issue) }
describe '#execute' do
let(:status_page_setting_enabled) { true }
let(:storage_client) { instance_double(StatusPage::Storage::S3Client) }
let(:status_page_setting) do
instance_double(StatusPageSetting, enabled?: status_page_setting_enabled,
storage_client: storage_client)
end
before do
stub_licensed_features(status_page: true)
allow(project).to receive(:status_page_setting)
.and_return(status_page_setting)
end
context 'when deletion succeeds' do
before do
allow(storage_client).to receive(:delete_object).with(key)
end
it 'removes details from CDN' do
expect(result).to be_success
expect(result.payload).to eq(object_key: key)
end
end
context 'when upload fails due to exception' do
let(:bucket) { 'bucket_name' }
let(:error) { StandardError.new }
let(:exception) do
StatusPage::Storage::Error.new(bucket: bucket, error: error)
end
before do
allow(storage_client).to receive(:delete_object).with(key)
.and_raise(exception)
end
it 'propagates the exception' do
expect { result }.to raise_error(exception)
end
end
context 'when status page setting is not enabled' do
let(:status_page_setting_enabled) { false }
it 'returns feature not available error' do
expect(result).to be_error
expect(result.message).to eq('Feature not available')
end
end
end
end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe StatusPage::PublishIncidentWorker do describe StatusPage::PublishWorker do
include ExclusiveLeaseHelpers include ExclusiveLeaseHelpers
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
...@@ -11,11 +11,11 @@ describe StatusPage::PublishIncidentWorker do ...@@ -11,11 +11,11 @@ describe StatusPage::PublishIncidentWorker do
let(:worker) { described_class.new } let(:worker) { described_class.new }
let(:logger) { worker.send(:logger) } let(:logger) { worker.send(:logger) }
let(:service) { instance_double(StatusPage::PublishIncidentService) } let(:service) { instance_double(StatusPage::PublishService) }
let(:service_result) { ServiceResponse.success } let(:service_result) { ServiceResponse.success }
before do before do
allow(StatusPage::PublishIncidentService) allow(StatusPage::PublishService)
.to receive(:new).with(user: user, project: project, issue_id: issue.id) .to receive(:new).with(user: user, project: project, issue_id: issue.id)
.and_return(service) .and_return(service)
allow(service).to receive(:execute) allow(service).to receive(:execute)
...@@ -40,7 +40,7 @@ describe StatusPage::PublishIncidentWorker do ...@@ -40,7 +40,7 @@ describe StatusPage::PublishIncidentWorker do
let(:project) { build(:project) } let(:project) { build(:project) }
it 'does not execute the service' do it 'does not execute the service' do
expect(StatusPage::PublishIncidentService).not_to receive(:execute) expect(StatusPage::PublishService).not_to receive(:execute)
subject subject
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