Commit f91731a8 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch '213014_instance_level_exportable_security_reports_api_changes' into 'master'

Instance level exportable security reports api changes

See merge request gitlab-org/gitlab!30397
parents 6231aa41 30886a18
# Project Vulnerabilities API **(ULTIMATE)**
# Vulnerability export API **(ULTIMATE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/197494) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/197494) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10. [Updated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30397) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.0.
CAUTION: **Caution:**
This API is currently in development and is protected by a **disabled**
......@@ -17,21 +17,21 @@ across GitLab releases.
Every API call to vulnerability exports must be [authenticated](README.md#authentication).
## Create a project-level vulnerability export
Creates a new vulnerability export for a project.
Vulnerability export permissions inherit permissions from their project. If a project is
private and a user isn't a member of the project to which the vulnerability
belongs, requests to that project return a `404 Not Found` status code.
Vulnerability exports can be only accessed by the export's author.
## Create vulnerability export
Creates a new vulnerability export.
If an authenticated user doesn't have permission to
[create a new vulnerability](../user/permissions.md#project-members-permissions),
this request results in a `403` status code.
```plaintext
POST /projects/:id/vulnerability_exports
POST /security/projects/:id/vulnerability_exports
```
| Attribute | Type | Required | Description |
......@@ -39,7 +39,7 @@ POST /projects/:id/vulnerability_exports
| `id` | integer or string | yes | The ID or [URL-encoded path](README.md#namespaced-path-encoding) of the project which the authenticated user is a member of |
```shell
curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/vulnerability_exports
curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/projects/1/vulnerability_exports
```
The created vulnerability export will be automatically deleted after 1 hour.
......@@ -56,8 +56,40 @@ Example response:
"started_at": null,
"finished_at": null,
"_links": {
"self": "https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2",
"download": "https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2/download"
"self": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2",
"download": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2/download"
}
}
```
## Create an instance-level vulnerability export
Creates a new vulnerability export for the projects of the user selected in the Security Dashboard.
```plaintext
POST /security/vulnerability_exports
```
```shell
curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/vulnerability_exports
```
The created vulnerability export is automatically deleted after one hour.
Example response:
```json
{
"id": 2,
"created_at": "2020-03-30T09:35:38.746Z",
"project_id": null,
"format": "csv",
"status": "created",
"started_at": null,
"finished_at": null,
"_links": {
"self": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2",
"download": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2/download"
}
}
```
......@@ -67,16 +99,15 @@ Example response:
Gets a single vulnerability export.
```plaintext
POST /projects/:id/vulnerability_exports/:vulnerability_export_id
GET /security/vulnerability_exports/:id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer or string | yes | The vulnerability's ID |
| `vulnerability_export_id` | integer or string | yes | The vulnerability export's ID |
| `id` | integer or string | yes | The vulnerability export's ID |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/vulnerability_exports/2
```
If the vulnerability export isn't finished, the response is `202 Accepted`.
......@@ -93,8 +124,8 @@ Example response:
"started_at": "2020-03-30T09:36:54.469Z",
"finished_at": "2020-03-30T09:36:55.008Z",
"_links": {
"self": "https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2",
"download": "https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2/download"
"self": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2",
"download": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2/download"
}
}
```
......@@ -104,16 +135,15 @@ Example response:
Downloads a single vulnerability export.
```plaintext
POST /projects/:id/vulnerability_exports/:vulnerability_export_id/download
GET /security/vulnerability_exports/:id/download
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer or string | yes | The vulnerability's ID |
| `vulnerability_export_id` | integer or string | yes | The vulnerability export's ID |
| `id` | integer or string | yes | The vulnerability export's ID |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2/download
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/vulnerability_exports/2/download
```
The response will be `404 Not Found` if the vulnerability export is not finished yet or was not found.
......
......@@ -248,7 +248,7 @@ module EE
def project_vulnerabilities_config(project)
return {} unless first_class_vulnerabilities_available?(project)
{ vulnerabilities_export_endpoint: api_v4_projects_vulnerability_exports_path(id: project.id) }
{ vulnerabilities_export_endpoint: api_v4_security_projects_vulnerability_exports_path(id: project.id) }
end
def can_create_feedback?(project, feedback_type)
......
......@@ -11,7 +11,8 @@ module SecurityHelper
vulnerable_projects_endpoint: security_vulnerable_projects_path,
vulnerabilities_endpoint: security_vulnerability_findings_path,
vulnerabilities_history_endpoint: history_security_vulnerability_findings_path,
vulnerability_feedback_help_path: help_page_path('user/application_security/index', anchor: 'interacting-with-the-vulnerabilities')
vulnerability_feedback_help_path: help_page_path('user/application_security/index', anchor: 'interacting-with-the-vulnerabilities'),
vulnerabilities_export_endpoint: expose_path(api_v4_security_vulnerability_exports_path)
}
end
end
# frozen_string_literal: true
class InstanceSecurityDashboardPolicy < BasePolicy
rule { ~anonymous }.policy do
enable :read_instance_security_dashboard
enable :create_vulnerability_export
with_scope :global
condition(:security_dashboard_enabled) do
License.feature_available?(:security_dashboard)
end
rule { ~anonymous }.enable :read_instance_security_dashboard
rule { security_dashboard_enabled & can?(:read_instance_security_dashboard) }.enable :create_vulnerability_export
end
......@@ -4,7 +4,7 @@
- license_management_settings_path = can?(current_user, :admin_software_license_policy, project) ? license_management_settings_path(project) : nil
- licenses_api_path = licenses_project_pipeline_path(project, pipeline) if project.feature_available?(:license_management)
- vulnerabilities_endpoint_path = expose_path(api_v4_projects_vulnerability_findings_path(id: project.id, params: { pipeline_id: pipeline.id, scope: 'dismissed' }))
- vulnerability_exports_endpoint_path = expose_path(api_v4_projects_vulnerability_exports_path(id: project.id))
- vulnerability_exports_endpoint_path = expose_path(api_v4_security_projects_vulnerability_exports_path(id: project.id))
- codequality_report_download_path = pipeline.downloadable_path_for_report_type(:codequality)
- if pipeline.expose_security_dashboard?
......
......@@ -7,7 +7,7 @@
#app{ data: { empty_state_svg_path: image_path('illustrations/security-dashboard_empty.svg'),
vulnerabilities_endpoint: expose_path(api_v4_projects_vulnerabilities_path(id: @project.id)),
vulnerability_exports_endpoint: expose_path(api_v4_projects_vulnerability_exports_path(id: @project.id)),
vulnerability_exports_endpoint: expose_path(api_v4_security_projects_vulnerability_exports_path(id: @project.id)),
project_full_path: @project.full_path,
dashboard_documentation: help_page_path('user/application_security/security_dashboard/index') } }
-# Display table loading animation while Vue app loads
......
---
title: Add API endpoints to generate instance level security report exports
merge_request: 30397
author:
type: added
......@@ -8,7 +8,20 @@ module API
helpers do
def vulnerability_export
strong_memoize(:vulnerability_export) do
user_project.vulnerability_exports.find(params[:export_id])
::Vulnerabilities::Export.find(params[:id])
end
end
def process_create_request_for(exportable)
vulnerability_export = ::VulnerabilityExports::CreateService.new(
exportable, current_user, format: params[:export_format]
).execute
if vulnerability_export.persisted?
status :created
present vulnerability_export, with: EE::API::Entities::VulnerabilityExport
else
render_validation_error!(vulnerability_export)
end
end
end
......@@ -17,42 +30,53 @@ module API
authenticate!
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
params do
optional :export_format, type: String, desc: 'The format of export to be generated',
default: ::Vulnerabilities::Export.formats.each_key.first,
values: ::Vulnerabilities::Export.formats.keys
end
desc 'Generate an export of project vulnerability findings' do
success EE::API::Entities::VulnerabilityExport
end
namespace :security do
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
params do
requires :id, type: String, desc: 'The ID of a project'
optional :export_format, type: String, desc: 'The format of export to be generated',
default: ::Vulnerabilities::Export.formats.each_key.first,
values: ::Vulnerabilities::Export.formats.keys
end
desc 'Generate an export of project vulnerability findings' do
success EE::API::Entities::VulnerabilityExport
end
before do
not_found! unless Feature.enabled?(:first_class_vulnerabilities, user_project)
end
post ':id/vulnerability_exports' do
authorize! :create_vulnerability_export, user_project
before do
not_found! unless Feature.enabled?(:first_class_vulnerabilities, user_project)
process_create_request_for(user_project)
end
end
post ':id/vulnerability_exports' do
authorize! :create_vulnerability_export, user_project
namespace do
before do
not_found! unless Feature.enabled?(:first_class_vulnerabilities)
end
vulnerability_export = ::VulnerabilityExports::CreateService.new(
user_project, current_user, format: params[:export_format]
).execute
params do
optional :export_format, type: String, desc: 'The format of export to be generated',
default: ::Vulnerabilities::Export.formats.each_key.first,
values: ::Vulnerabilities::Export.formats.keys
end
desc 'Generate an instance level export' do
success EE::API::Entities::VulnerabilityExport
end
post 'vulnerability_exports' do
authorize! :create_vulnerability_export, current_user.security_dashboard
if vulnerability_export.persisted?
status :created
present vulnerability_export, with: EE::API::Entities::VulnerabilityExport
else
render_validation_error!(vulnerability_export)
process_create_request_for(current_user.security_dashboard)
end
end
desc 'Get single project vulnerability export' do
success EE::API::Entities::VulnerabilityExport
end
get ':id/vulnerability_exports/:export_id' do
get 'vulnerability_exports/:id' do
authorize! :read_vulnerability_export, vulnerability_export
unless vulnerability_export.completed?
......@@ -65,7 +89,7 @@ module API
end
desc 'Download single project vulnerability export'
get ':id/vulnerability_exports/:export_id/download' do
get 'vulnerability_exports/:id/download' do
authorize! :read_vulnerability_export, vulnerability_export
if vulnerability_export.finished?
......
......@@ -16,11 +16,11 @@ module EE
expose :_links do
expose :self do |export|
expose_url api_v4_projects_vulnerability_exports_path(id: export.project_id, export_id: export.id)
expose_url api_v4_security_vulnerability_exports_path(id: export.id)
end
expose :download do |export|
expose_url api_v4_projects_vulnerability_exports_download_path(id: export.project_id, export_id: export.id)
expose_url api_v4_security_vulnerability_exports_download_path(id: export.id)
end
end
end
......
......@@ -134,7 +134,7 @@ describe ProjectsHelper do
it 'checks if first vulnerability class is enabled' do
expect(subject[:vulnerabilities_export_endpoint]).to(
eq(
api_v4_projects_vulnerability_exports_path(id: project.id)
api_v4_security_projects_vulnerability_exports_path(id: project.id)
))
end
end
......
......@@ -20,7 +20,8 @@ describe SecurityHelper do
vulnerable_projects_endpoint: security_vulnerable_projects_path,
vulnerabilities_endpoint: security_vulnerability_findings_path,
vulnerabilities_history_endpoint: history_security_vulnerability_findings_path,
vulnerability_feedback_help_path: help_page_path('user/application_security/index', anchor: 'interacting-with-the-vulnerabilities')
vulnerability_feedback_help_path: help_page_path('user/application_security/index', anchor: 'interacting-with-the-vulnerabilities'),
vulnerabilities_export_endpoint: api_v4_security_vulnerability_exports_path
})
end
end
......
......@@ -18,8 +18,8 @@ describe ::EE::API::Entities::VulnerabilityExport do
expect(subject[:status]).to eq(vulnerability_export.status)
expect(subject[:started_at]).to eq(vulnerability_export.started_at)
expect(subject[:finished_at]).to eq(vulnerability_export.finished_at)
expect(subject[:_links][:self]).to end_with("api/v4/projects/#{vulnerability_export.project_id}/vulnerability_exports/#{vulnerability_export.id}")
expect(subject[:_links][:download]).to end_with("api/v4/projects/#{vulnerability_export.project_id}/vulnerability_exports/#{vulnerability_export.id}/download")
expect(subject[:_links][:self]).to end_with("api/v4/security/vulnerability_exports/#{vulnerability_export.id}")
expect(subject[:_links][:download]).to end_with("api/v4/security/vulnerability_exports/#{vulnerability_export.id}/download")
end
end
end
......@@ -12,13 +12,11 @@ describe API::VulnerabilityExports do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :with_vulnerability) }
let(:project_vulnerability_exports_path) { "/projects/#{project.id}/vulnerability_exports" }
let(:project_vulnerability_export_path) { "#{project_vulnerability_exports_path}/#{vulnerability_export.id}" }
describe 'POST /projects/:id/vulnerability_exports' do
describe 'POST /security/projects/:id/vulnerability_exports' do
let(:format) { 'csv' }
let(:request_path) { "/security/projects/#{project.id}/vulnerability_exports" }
subject(:create_vulnerability_export) { post api(project_vulnerability_exports_path, user), params: { export_format: format } }
subject(:create_vulnerability_export) { post api(request_path, user), params: { export_format: format } }
context 'with an authorized user with proper permissions' do
before do
......@@ -73,10 +71,63 @@ describe API::VulnerabilityExports do
end
end
describe 'GET /projects/:id/vulnerability_exports/:export_id' do
describe 'POST /security/vulnerability_exports' do
let(:format) { 'csv' }
let(:request_path) { "/security/vulnerability_exports" }
subject(:create_vulnerability_export) { post api(request_path, user), params: { export_format: format } }
context 'when the request does not fulfill the requirements' do
let(:format) { 'exif' }
it 'responds with bad_request' do
create_vulnerability_export
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq('error' => 'export_format does not have a valid value')
end
end
context 'when the request fulfills the requirements' do
let(:mock_service_object) { instance_double(VulnerabilityExports::CreateService, execute: vulnerability_export) }
before do
allow(VulnerabilityExports::CreateService).to receive(:new).and_return(mock_service_object)
end
context 'when the export creation succeeds' do
let(:vulnerability_export) { create(:vulnerability_export) }
it 'returns information about new vulnerability export' do
create_vulnerability_export
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('public_api/v4/vulnerability_export', dir: 'ee')
end
end
context 'when the export creation fails' do
let(:errors) { instance_double(ActiveModel::Errors, any?: true, messages: ['foo']) }
let(:vulnerability_export) { instance_double(Vulnerabilities::Export, persisted?: false, errors: errors) }
it 'returns the error message' do
create_vulnerability_export
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq('message' => ['foo'])
end
end
end
it_behaves_like 'forbids access to vulnerability API endpoint in case of disabled features'
end
describe 'GET /security/vulnerability_exports/:id' do
let_it_be(:vulnerability_export) { create(:vulnerability_export, :finished, :csv, :with_csv_file, project: project, author: user) }
subject(:get_vulnerability_export) { get api(project_vulnerability_export_path, user) }
let(:request_path) { "/security/vulnerability_exports/#{vulnerability_export.id}" }
subject(:get_vulnerability_export) { get api(request_path, user) }
context 'with an authorized user with proper permissions' do
before do
......@@ -116,8 +167,6 @@ describe API::VulnerabilityExports do
expect(response.headers['Poll-Interval']).to eq '5000'
end
end
it_behaves_like 'forbids access to vulnerability API endpoint in case of disabled features'
end
describe 'permissions' do
......@@ -140,10 +189,11 @@ describe API::VulnerabilityExports do
end
end
describe 'GET /projects/:id/vulnerability_exports/:export_id/download' do
describe 'GET /security/vulnerability_exports/:id/download' do
let!(:vulnerability_export) { create(:vulnerability_export, :finished, :csv, :with_csv_file, project: project, author: user) }
let(:request_path) { "/security/vulnerability_exports/#{vulnerability_export.id}/download" }
subject(:download_vulnerability_export) { get api("#{project_vulnerability_export_path}/download", user) }
subject(:download_vulnerability_export) { get api(request_path, user) }
context 'with an authorized user with proper permissions' do
before do
......@@ -181,8 +231,6 @@ describe API::VulnerabilityExports do
expect(response.headers['Poll-Interval']).to be_blank
end
end
it_behaves_like 'forbids access to vulnerability API endpoint in case of disabled features'
end
describe 'permissions' do
......
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