Commit 59d90368 authored by Peter Leitzen's avatar Peter Leitzen

Unpublish details for confidential issues

* Implement deleting objects for S3 client
* Implement unpublishing service
* Rename publish to process
* Allow finding a confidential issue for status page
parent 6e96907b
...@@ -10,10 +10,10 @@ ...@@ -10,10 +10,10 @@
# #
# finder = StatusPage::IncidentsFinder.new(project_id: project_id) # finder = StatusPage::IncidentsFinder.new(project_id: project_id)
# #
# # A single issue # # A single issue (including confidential issue)
# issue, user_notes = finder.find_by_id(issue_id) # issue = finder.find_by_id(issue_id)
# #
# # Most recent 20 issues # # Most recent 20 non-confidential issues
# issues = finder.all # issues = finder.all
# #
module StatusPage module StatusPage
...@@ -25,23 +25,22 @@ module StatusPage ...@@ -25,23 +25,22 @@ module StatusPage
end end
def find_by_id(issue_id) def find_by_id(issue_id)
execute.find_by_id(issue_id) execute(include_confidential: true)
.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)
......
...@@ -10,7 +10,7 @@ module StatusPage ...@@ -10,7 +10,7 @@ module StatusPage
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
......
...@@ -9,6 +9,7 @@ module StatusPage ...@@ -9,6 +9,7 @@ module StatusPage
# #
# This services calls: # This services calls:
# * StatusPage::PublishDetailsService # * StatusPage::PublishDetailsService
# * StatusPage::UnpublishDetailsService
# * StatusPage::PublishListService # * StatusPage::PublishListService
class PublishIncidentService class PublishIncidentService
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
...@@ -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
......
...@@ -10,7 +10,7 @@ module StatusPage ...@@ -10,7 +10,7 @@ module StatusPage
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)
......
# 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::PublishIncidentService+ and is not meant to be called directly.
#
# Consider calling +StatusPage::PublishIncidentService+ 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
...@@ -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
......
...@@ -28,7 +28,7 @@ describe StatusPage::IncidentsFinder do ...@@ -28,7 +28,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
...@@ -40,6 +40,7 @@ describe StatusPage::IncidentsFinder do ...@@ -40,6 +40,7 @@ describe StatusPage::IncidentsFinder do
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 +59,13 @@ describe StatusPage::IncidentsFinder do ...@@ -58,5 +59,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)
......
...@@ -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
# 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
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