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,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
end
end
context 'when upload fails' do
it 'propagates the exception' do
expect_to_upload_details(issue, status: 404)
expect(result).to be_success 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)
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) 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
# 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