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
...@@ -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,7 +17,10 @@ describe StatusPage::IncidentsFinder do ...@@ -17,7 +17,10 @@ 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 'without params' do
let(:params) { {} }
context 'for public issue' do context 'for public issue' do
let(:issue) { public_issues.first } let(:issue) { public_issues.first }
...@@ -28,7 +31,7 @@ describe StatusPage::IncidentsFinder do ...@@ -28,7 +31,7 @@ describe StatusPage::IncidentsFinder do
context 'for confidential issue' do context 'for confidential issue' do
let(:issue) { issues.fetch(:confidential) } let(:issue) { issues.fetch(:confidential) }
it { is_expected.to be_nil } it { is_expected.to eq(issue) }
end end
context 'for unrelated issue' do context 'for unrelated issue' do
...@@ -38,8 +41,20 @@ describe StatusPage::IncidentsFinder do ...@@ -38,8 +41,20 @@ describe StatusPage::IncidentsFinder do
end end
end end
context 'with include_confidential' do
let(:params) { { include_confidential: false } }
context 'for confidential issue' do
let(:issue) { issues.fetch(:confidential) }
it { is_expected.to be_nil }
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,8 +23,9 @@ describe StatusPage::PublishIncidentService do ...@@ -23,8 +23,9 @@ 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
it 'uploads incident details and list' do
expect_to_upload_details(issue) expect_to_upload_details(issue)
expect_to_upload_list expect_to_upload_list
...@@ -32,15 +33,38 @@ describe StatusPage::PublishIncidentService do ...@@ -32,15 +33,38 @@ describe StatusPage::PublishIncidentService do
end end
end end
context 'when uploading details fails' do context 'when upload fails' do
it 'propagates the exception' do it 'propagates the exception' do
expect_to_upload_details(issue, status: 404) expect_to_upload_details(issue, status: 404)
expect { result }.to raise_error(StatusPage::Storage::Error) expect { result }.to raise_error(StatusPage::Storage::Error)
end end
end end
end
describe 'unpublish details' do
let_it_be(:issue) { create(:issue, :confidential, project: project) }
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)
context 'when uploading list fails' do expect { result }.to raise_error(StatusPage::Storage::Error)
end
end
end
describe 'publish list' do
context 'when upload fails' do
it 'returns error and skip list upload' do it 'returns error and skip list upload' do
expect_to_upload_details(issue) expect_to_upload_details(issue)
expect_to_upload_list(status: 404) expect_to_upload_list(status: 404)
...@@ -48,6 +72,7 @@ describe StatusPage::PublishIncidentService do ...@@ -48,6 +72,7 @@ describe StatusPage::PublishIncidentService do
expect { result }.to raise_error(StatusPage::Storage::Error) expect { result }.to raise_error(StatusPage::Storage::Error)
end end
end end
end
context 'with unrelated issue' do context 'with unrelated issue' do
let(:issue) { create(:issue) } let(:issue) { create(:issue) }
...@@ -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