Commit 8adf0447 authored by Sean Arnold's avatar Sean Arnold Committed by Alex Pooley

Add read API for Alert Metric Images

Changelog: added
EE: true
parent 66f07af3
......@@ -40,30 +40,29 @@ class UploadsController < ApplicationController
upload_model_class.find(params[:id])
end
def authorize_access!
authorized =
case model
when Note
can?(current_user, :read_project, model.project)
when Snippet, ProjectSnippet
can?(current_user, :read_snippet, model)
when User
# We validate the current user has enough (writing)
# access to itself when a secret is given.
# For instance, user avatars are readable by anyone,
# while temporary, user snippet uploads are not.
!secret? || can?(current_user, :update_user, model)
when Appearance
true
when Projects::Topic
true
else
permission = "read_#{model.class.underscore}".to_sym
can?(current_user, permission, model)
end
def authorized?
case model
when Note
can?(current_user, :read_project, model.project)
when Snippet, ProjectSnippet
can?(current_user, :read_snippet, model)
when User
# We validate the current user has enough (writing)
# access to itself when a secret is given.
# For instance, user avatars are readable by anyone,
# while temporary, user snippet uploads are not.
!secret? || can?(current_user, :update_user, model)
when Appearance
true
when Projects::Topic
true
else
can?(current_user, "read_#{model.class.underscore}".to_sym, model)
end
end
render_unauthorized unless authorized
def authorize_access!
render_unauthorized unless authorized?
end
def authorize_create_access!
......
......@@ -5,3 +5,5 @@ module AlertManagement
delegate { @subject.project }
end
end
AlertManagement::AlertPolicy.prepend_mod
---
stage: Monitor
group: Respond
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Alert Management Alerts API **(FREE)**
This is the documentation of Alert Management Alerts API.
NOTE:
This API is limited to metric images. For more API endpoints please refer to the [GraphQL API](graphql/reference/index.md#alertmanagementalert).
## List metric images
```plaintext
GET /projects/:id/alert_management_alerts/:alert_iid/metric_images
```
| Attribute | Type | Required | Description |
|-------------|---------|----------|--------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user |
| `alert_iid` | integer | yes | The internal ID of a project's alert |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/alert_management_alerts/93/metric_images"
```
Example response:
```json
[
{
"id": 17,
"created_at": "2020-11-12T20:07:58.156Z",
"filename": "sample_2054",
"file_path": "/uploads/-/system/alert_metric_image/file/17/sample_2054.png",
"url": "example.com/metric",
"url_text": "An example metric"
},
{
"id": 18,
"created_at": "2020-11-12T20:14:26.441Z",
"filename": "sample_2054",
"file_path": "/uploads/-/system/alert_metric_image/file/18/sample_2054.png",
"url": "example.com/metric",
"url_text": "An example metric"
}
]
```
......@@ -3,9 +3,11 @@
module EE
module UploadsController
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
EE_MODEL_CLASSES = {
'issuable_metric_image' => IssuableMetricImage
'issuable_metric_image' => IssuableMetricImage,
'alert_management_metric_image' => ::AlertManagement::MetricImage
}.freeze
class_methods do
......@@ -16,5 +18,17 @@ module EE
super.merge(EE_MODEL_CLASSES)
end
end
override :authorized?
def authorized?
case model
when IssuableMetricImage
can?(current_user, :read_issuable_metric_image, model)
when ::AlertManagement::MetricImage
can?(current_user, :read_alert_management_metric_image, model.alert)
else
super
end
end
end
end
......@@ -2,12 +2,20 @@
module AlertManagement
class MetricImage < ApplicationRecord
include MetricImageUploading
self.table_name = 'alert_management_alert_metric_images'
belongs_to :alert, class_name: 'AlertManagement::Alert', foreign_key: 'alert_id', inverse_of: :metric_images
validates :file, presence: true
validates :url, length: { maximum: 255 }, public_url: { allow_blank: true }
validates :url_text, length: { maximum: 128 }
private
def local_path
Gitlab::Routing.url_helpers.alert_metric_image_upload_path(
filename: file.filename,
id: file.upload.model_id,
model: model_name.param_key,
mounted_as: 'file'
)
end
end
end
# frozen_string_literal: true
module MetricImageUploading
extend ActiveSupport::Concern
MAX_FILE_SIZE = 1.megabyte.freeze
included do
include Gitlab::FileTypeDetection
include FileStoreMounter
include WithUploads
validates :file, presence: true
validate :validate_file_is_image
validates :url, length: { maximum: 255 }, public_url: { allow_blank: true }
validates :url_text, length: { maximum: 128 }
scope :order_created_at_asc, -> { order(created_at: :asc) }
attribute :file_store, :integer, default: -> { MetricImageUploader.default_store }
mount_file_store_uploader MetricImageUploader
end
def filename
@filename ||= file&.filename
end
def file_path
@file_path ||= begin
return file&.url unless file&.upload
# If we're using a CDN, we need to use the full URL
asset_host = ActionController::Base.asset_host || Gitlab.config.gitlab.base_url
Gitlab::Utils.append_path(asset_host, local_path)
end
end
private
def valid_file_extensions
Gitlab::FileTypeDetection::SAFE_IMAGE_EXT
end
def validate_file_is_image
unless image?
message = _('does not have a supported extension. Only %{extension_list} are supported') % {
extension_list: valid_file_extensions.to_sentence
}
errors.add(:file, message)
end
end
end
# frozen_string_literal: true
class IssuableMetricImage < ApplicationRecord
include Gitlab::FileTypeDetection
include FileStoreMounter
include WithUploads
include MetricImageUploading
belongs_to :issue, class_name: 'Issue', foreign_key: 'issue_id', inverse_of: :metric_images
attribute :file_store, :integer, default: -> { IssuableMetricImageUploader.default_store }
mount_file_store_uploader IssuableMetricImageUploader
validates :issue, presence: true
validates :file, presence: true
validate :validate_file_is_image
validates :url, length: { maximum: 255 }, public_url: { allow_blank: true }
validates :url_text, length: { maximum: 128 }
scope :order_created_at_asc, -> { order(created_at: :asc) }
MAX_FILE_SIZE = 1.megabyte.freeze
def self.available_for?(project)
project&.feature_available?(:incident_metric_upload)
end
def filename
@filename ||= file&.filename
end
def file_path
@file_path ||= begin
return file&.url unless file&.upload
# If we're using a CDN, we need to use the full URL
asset_host = ActionController::Base.asset_host || Gitlab.config.gitlab.base_url
local_path = Gitlab::Routing.url_helpers.issuable_metric_image_upload_path(
filename: file.filename,
id: file.upload.model_id,
model: self.class.name.underscore,
mounted_as: 'file'
)
Gitlab::Utils.append_path(asset_host, local_path)
end
end
private
def valid_file_extensions
SAFE_IMAGE_EXT
end
def validate_file_is_image
unless image?
message = _('does not have a supported extension. Only %{extension_list} are supported') % {
extension_list: valid_file_extensions.to_sentence
}
errors.add(:file, message)
end
def local_path
Gitlab::Routing.url_helpers.issuable_metric_image_upload_path(
filename: file.filename,
id: file.upload.model_id,
model: model_name.param_key,
mounted_as: 'file'
)
end
end
# frozen_string_literal: true
module EE
module AlertManagement
module AlertPolicy
extend ActiveSupport::Concern
prepended do
rule { can?(:read_alert_management_alert) }.policy do
enable :read_alert_management_metric_image
end
rule { can?(:update_alert_management_alert) }.policy do
enable :upload_alert_management_metric_image
enable :destroy_alert_management_metric_image
end
end
end
end
end
# frozen_string_literal: true
class IssuableMetricImageUploader < GitlabUploader
class MetricImageUploader < GitlabUploader # rubocop:disable Gitlab/NamespacedClass
include RecordsUploads::Concern
include ObjectStorage::Concern
prepend ObjectStorage::Extension::RecordsUploads
......
......@@ -6,4 +6,10 @@ scope path: :uploads do
to: "uploads#show",
constraints: { model: /issuable_metric_image/, mounted_as: /file/, filename: %r{[^/]+} },
as: 'issuable_metric_image_upload'
# Alert Metric Images
get "-/system/:model/:mounted_as/:id/:filename",
to: "uploads#show",
constraints: { model: /alert_management_metric_image/, mounted_as: /file/, filename: %r{[^/]+} },
as: 'alert_metric_image_upload'
end
# frozen_string_literal: true
module API
class AlertManagementAlerts < ::API::Base
feature_category :incident_management
params do
requires :id, type: String, desc: 'The ID of a project'
requires :alert_iid, type: Integer, desc: 'The IID of the Alert'
end
resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
namespace ':id/alert_management_alerts/:alert_iid/metric_images' do
desc 'Metric Images for alert'
get do
alert = find_project_alert(params[:alert_iid])
if can?(current_user, :read_alert_management_metric_image, alert)
present alert.metric_images.order_created_at_asc, with: Entities::MetricImage
else
render_api_error!('Alert not found', 404)
end
end
end
end
helpers do
def find_project_alert(iid, project_id = nil)
project = project_id ? find_project!(project_id) : user_project
::AlertManagement::AlertsFinder.new(current_user, project, { iid: [iid] }).execute.first
end
end
end
end
# frozen_string_literal: true
module API
module Entities
class MetricImage < Grape::Entity
expose :id, :created_at, :filename, :file_path, :url, :url_text
end
end
end
......@@ -10,6 +10,7 @@ module EE
mount ::EE::API::GroupBoards
mount ::API::AlertManagementAlerts
mount ::API::AuditEvents
mount ::API::ProjectApprovalRules
mount ::API::StatusChecks
......
......@@ -25,7 +25,7 @@ module EE
maximum_size: ::IssuableMetricImage::MAX_FILE_SIZE.to_i
}
::IssuableMetricImageUploader.workhorse_authorize(**params)
::MetricImageUploader.workhorse_authorize(**params)
end
desc 'Upload a metric image for an issue' do
......
......@@ -6,7 +6,7 @@ module EE
module MigrationHelper
extend ActiveSupport::Concern
EE_CATEGORIES = [%w(IssuableMetricImageUploader IssuableMetricImage :file)].freeze
EE_CATEGORIES = [%w(MetricImageUploader IssuableMetricImage :file)].freeze
class_methods do
extend ::Gitlab::Utils::Override
......
......@@ -12,6 +12,7 @@ module Gitlab
file
import_export
issuable_metric_image
metric_image
namespace_file
personal_file
].freeze
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe UploadsController do
let!(:user) { create(:user) }
let!(:project) { create(:project) }
describe "GET show" do
context 'when viewing issuable metric images' do
let(:incident) { create(:incident, project: project) }
let(:metric_image) { create(:issuable_metric_image, issue: incident) }
before do
project.add_developer(user)
sign_in(user)
end
it "responds with status 200" do
get :show, params: { model: "issuable_metric_image", mounted_as: 'file', id: metric_image.id, filename: metric_image.filename }
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'when viewing alert metric images' do
let(:alert) { create(:alert_management_alert, project: project) }
let(:metric_image) { create(:alert_metric_image, alert: alert) }
before do
project.add_developer(user)
sign_in(user)
end
it "responds with status 200" do
get :show, params: { model: "alert_management_metric_image", mounted_as: 'file', id: metric_image.id, filename: metric_image.filename }
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end
......@@ -5,7 +5,7 @@ FactoryBot.modify do
trait :issue_metric_image do
model { association(:issuable_metric_image) }
mount_point { :file }
uploader { ::IssuableMetricImageUploader.name }
uploader { ::MetricImageUploader.name }
end
trait(:verification_succeeded) do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe AlertManagement::AlertPolicy do
describe '#rules' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:alert) { create(:alert_management_alert, project: project, issue: incident) }
let_it_be(:incident) { nil }
let(:policy) { described_class.new(user, alert) }
describe 'rules' do
shared_examples 'does not allow metric image reads' do
it { expect(policy).to be_disallowed(:read_alert_management_metric_image) }
end
shared_examples 'does not allow metric image updates' do
specify do
expect(policy).to be_disallowed(:upload_alert_management_metric_image)
expect(policy).to be_disallowed(:destroy_alert_management_metric_image)
end
end
shared_examples 'allows metric image reads' do
it { expect(policy).to be_allowed(:read_alert_management_metric_image) }
end
shared_examples 'allows metric image updates' do
specify do
expect(policy).to be_allowed(:upload_alert_management_metric_image)
expect(policy).to be_allowed(:destroy_alert_management_metric_image)
end
end
context 'when user is not a member' do
include_examples 'does not allow metric image reads'
include_examples 'does not allow metric image updates'
end
context 'when user is a guest' do
before do
project.add_guest(user)
end
include_examples 'does not allow metric image reads'
include_examples 'does not allow metric image updates'
end
context 'when user is a developer' do
before do
project.add_developer(user)
end
include_examples 'allows metric image reads'
include_examples 'allows metric image updates'
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::AlertManagementAlerts do
let_it_be(:creator) { create(:user) }
let_it_be(:project) do
create(:project, :public, creator_id: creator.id, namespace: creator.namespace)
end
let_it_be(:user) { create(:user) }
let_it_be(:alert) { create(:alert_management_alert, project: project) }
describe 'GET /projects/:id/alert_management_alerts/:alert_iid/metric_images' do
using RSpec::Parameterized::TableSyntax
let!(:image) { create(:alert_metric_image, alert: alert) }
subject { get api("/projects/#{project.id}/alert_management_alerts/#{alert.iid}/metric_images", user) }
shared_examples 'can_read_metric_image' do
it 'can read the metric images' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.first).to match(
{
id: image.id,
created_at: image.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
filename: image.filename,
file_path: image.file_path,
url: image.url,
url_text: nil
}.with_indifferent_access
)
end
end
shared_examples 'unauthorized_read' do
it 'cannot read the metric images' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
where(:user_role, :public_project, :expected_status) do
:not_member | false | :unauthorized_read
:not_member | true | :unauthorized_read
:guest | false | :unauthorized_read
:reporter | false | :unauthorized_read
:developer | false | :can_read_metric_image
end
with_them do
before do
stub_licensed_features(alert_metric_upload: true)
project.send("add_#{user_role}", user) unless user_role == :not_member
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) unless public_project
end
it_behaves_like "#{params[:expected_status]}"
end
end
end
......@@ -188,7 +188,7 @@ RSpec.describe API::Issues, :mailer do
end
context 'filtering by iteration' do
let_it_be(:iteration_1) { create(:iteration, group: group, start_date: Date.today) }
let_it_be(:iteration_1) { create(:iteration, group: group, start_date: Date.current) }
let_it_be(:iteration_2) { create(:iteration, group: group) }
let_it_be(:iteration_1_issue) { create(:issue, project: group_project, iteration: iteration_1) }
let_it_be(:iteration_2_issue) { create(:issue, project: group_project, iteration: iteration_2) }
......@@ -572,6 +572,69 @@ RSpec.describe API::Issues, :mailer do
end
end
describe 'PUT /projects/:id/issues/:issue_iid/metric_images/authorize' do
include_context 'workhorse headers'
let_it_be(:issue) { create(:incident, project: project) }
before do
project.add_developer(user)
end
subject { post api("/projects/#{project.id}/issues/#{issue.iid}/metric_images/authorize", user), headers: workhorse_headers }
it 'authorizes uploading with workhorse header' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
it 'rejects requests that bypassed gitlab-workhorse' do
workhorse_headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
context 'when using remote storage' do
context 'when direct upload is enabled' do
before do
stub_uploads_object_storage(MetricImageUploader, enabled: true, direct_upload: true)
end
it 'responds with status 200, location of file remote store and object details' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response).not_to have_key('TempPath')
expect(json_response['RemoteObject']).to have_key('ID')
expect(json_response['RemoteObject']).to have_key('GetURL')
expect(json_response['RemoteObject']).to have_key('StoreURL')
expect(json_response['RemoteObject']).to have_key('DeleteURL')
expect(json_response['RemoteObject']).to have_key('MultipartUpload')
end
end
context 'when direct upload is disabled' do
before do
stub_uploads_object_storage(MetricImageUploader, enabled: true, direct_upload: false)
end
it 'handles as a local file' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response['TempPath']).to eq(MetricImageUploader.workhorse_local_upload_path)
expect(json_response['RemoteObject']).to be_nil
end
end
end
end
describe 'POST /projects/:id/issues/:issue_iid/metric_images' do
include WorkhorseHelpers
using RSpec::Parameterized::TableSyntax
......@@ -606,7 +669,7 @@ RSpec.describe API::Issues, :mailer do
expect(json_response['filename']).to eq(file_name)
expect(json_response['url']).to eq(url)
expect(json_response['url_text']).to eq(url_text)
expect(json_response['file_path']).to match(%r{/uploads/-/system/issuable_metric_image/file/[\d+]/#{file_name}})
expect(json_response['file_path']).to match(%r{/uploads/-/system/issuable_metric_image/file/[\d+]+/#{file_name}})
expect(json_response['created_at']).not_to be_nil
expect(json_response['id']).not_to be_nil
end
......@@ -631,8 +694,8 @@ RSpec.describe API::Issues, :mailer do
with_them do
before do
# Local storage
stub_uploads_object_storage(IssuableMetricImageUploader, enabled: false)
allow_any_instance_of(IssuableMetricImageUploader).to receive(:file_storage?).and_return(true)
stub_uploads_object_storage(MetricImageUploader, enabled: false)
allow_any_instance_of(MetricImageUploader).to receive(:file_storage?).and_return(true)
stub_licensed_features(incident_metric_upload: true)
project.send("add_#{user_role}", user2)
......@@ -662,9 +725,9 @@ RSpec.describe API::Issues, :mailer do
before do
# Object storage
stub_licensed_features(incident_metric_upload: true)
stub_uploads_object_storage(IssuableMetricImageUploader)
stub_uploads_object_storage(MetricImageUploader)
allow_any_instance_of(IssuableMetricImageUploader).to receive(:file_storage?).and_return(false)
allow_any_instance_of(MetricImageUploader).to receive(:file_storage?).and_return(false)
project.add_developer(user2)
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Uploads', 'routing' do
it 'allows fetching issuable metric images' do
expect(get('/uploads/-/system/issuable_metric_image/file/1/test.jpg')).to route_to(
controller: 'uploads',
action: 'show',
model: 'issuable_metric_image',
id: '1',
filename: 'test.jpg',
mounted_as: 'file'
)
end
it 'allows fetching alert metric metric images' do
expect(get('/uploads/-/system/alert_management_metric_image/file/1/test.jpg')).to route_to(
controller: 'uploads',
action: 'show',
model: 'alert_management_metric_image',
id: '1',
filename: 'test.jpg',
mounted_as: 'file'
)
end
end
......@@ -158,7 +158,7 @@ RSpec.describe Geo::FileUploadService do
context 'incident metrics upload' do
let(:incident_metric_image) { create(:issuable_metric_image) }
let(:upload) { Upload.find_by(model: incident_metric_image, uploader: ::IssuableMetricImageUploader.name) }
let(:upload) { Upload.find_by(model: incident_metric_image, uploader: ::MetricImageUploader.name) }
let(:params) { { id: upload.id, type: 'issuable_metric_image' } }
let(:request_data) { Gitlab::Geo::Replication::FileTransfer.new(:file, upload).request_data }
......
......@@ -16,8 +16,8 @@ RSpec.describe 'gitlab:uploads:migrate and migrate_to_local rake tasks', :silenc
allow(ObjectStorage::MigrateUploadsWorker).to receive(:perform_async)
end
context 'for IssuableMetricImageUploader' do
let(:uploader_class) { IssuableMetricImageUploader }
context 'for MetricImageUploader' do
let(:uploader_class) { MetricImageUploader }
let(:model_class) { IssuableMetricImage }
let(:mounted_as) { :file }
......
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