Commit 204d3d33 authored by Ash McKenzie's avatar Ash McKenzie

Merge branch '213013_group_level_security_reports_api' into 'master'

Group-level vulnerability exports API

See merge request gitlab-org/gitlab!31889
parents d76c5e68 d9391e55
...@@ -42,7 +42,7 @@ POST /security/projects/:id/vulnerability_exports ...@@ -42,7 +42,7 @@ POST /security/projects/:id/vulnerability_exports
curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/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. The created vulnerability export is automatically deleted after 1 hour.
Example response: Example response:
...@@ -51,6 +51,53 @@ Example response: ...@@ -51,6 +51,53 @@ Example response:
"id": 2, "id": 2,
"created_at": "2020-03-30T09:35:38.746Z", "created_at": "2020-03-30T09:35:38.746Z",
"project_id": 1, "project_id": 1,
"group_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"
}
}
```
## Create a group-level vulnerability export
Creates a new vulnerability export for a group.
Vulnerability export permissions inherit permissions from their group. If a group is
private and a user isn't a member of the group to which the vulnerability
belongs, requests to that group return a `404 Not Found` status code.
Vulnerability exports can be only accessed by the export's author.
If an authenticated user doesn't have permission to
[create a new vulnerability](../user/permissions.md#group-members-permissions),
this request results in a `403` status code.
```plaintext
POST /security/groups/:id/vulnerability_exports
```
| Attribute | Type | Required | Description |
| ------------------- | ----------------- | ---------- | -----------------------------------------------------------------------------------------------------------------------------|
| `id` | integer or string | yes | The ID or [URL-encoded path](README.md#namespaced-path-encoding) of the group which the authenticated user is a member of |
```shell
curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/groups/1/vulnerability_exports
```
The created vulnerability export is automatically deleted after 1 hour.
Example response:
```json
{
"id": 2,
"created_at": "2020-03-30T09:35:38.746Z",
"project_id": null,
"group_id": 1,
"format": "csv", "format": "csv",
"status": "created", "status": "created",
"started_at": null, "started_at": null,
...@@ -83,6 +130,7 @@ Example response: ...@@ -83,6 +130,7 @@ Example response:
"id": 2, "id": 2,
"created_at": "2020-03-30T09:35:38.746Z", "created_at": "2020-03-30T09:35:38.746Z",
"project_id": null, "project_id": null,
"group_id": null,
"format": "csv", "format": "csv",
"status": "created", "status": "created",
"started_at": null, "started_at": null,
...@@ -119,6 +167,7 @@ Example response: ...@@ -119,6 +167,7 @@ Example response:
"id": 2, "id": 2,
"created_at": "2020-03-30T09:35:38.746Z", "created_at": "2020-03-30T09:35:38.746Z",
"project_id": 1, "project_id": 1,
"group_id": null,
"format": "csv", "format": "csv",
"status": "finished", "status": "finished",
"started_at": "2020-03-30T09:36:54.469Z", "started_at": "2020-03-30T09:36:54.469Z",
......
...@@ -202,6 +202,8 @@ module EE ...@@ -202,6 +202,8 @@ module EE
rule { security_dashboard_enabled & developer }.enable :read_group_security_dashboard rule { security_dashboard_enabled & developer }.enable :read_group_security_dashboard
rule { can?(:read_group_security_dashboard) }.enable :create_vulnerability_export
rule { admin | owner }.policy do rule { admin | owner }.policy do
enable :read_group_compliance_dashboard enable :read_group_compliance_dashboard
enable :read_group_credentials_inventory enable :read_group_credentials_inventory
......
---
title: Introduce a new API endpoint to generate group-level vulnerability exports
merge_request: 31889
author:
type: added
...@@ -38,7 +38,7 @@ module API ...@@ -38,7 +38,7 @@ module API
default: ::Vulnerabilities::Export.formats.each_key.first, default: ::Vulnerabilities::Export.formats.each_key.first,
values: ::Vulnerabilities::Export.formats.keys values: ::Vulnerabilities::Export.formats.keys
end end
desc 'Generate an export of project vulnerability findings' do desc 'Generate a project-level export' do
success EE::API::Entities::VulnerabilityExport success EE::API::Entities::VulnerabilityExport
end end
...@@ -53,6 +53,28 @@ module API ...@@ -53,6 +53,28 @@ module API
end end
end end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
params do
requires :id, type: String, desc: 'The ID of a group'
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 a group-level export' do
success EE::API::Entities::VulnerabilityExport
end
before do
not_found! unless Feature.enabled?(:first_class_vulnerabilities, user_group, default_enabled: true)
end
post ':id/vulnerability_exports' do
authorize! :create_vulnerability_export, user_group
process_create_request_for(user_group)
end
end
namespace do namespace do
before do before do
not_found! unless Feature.enabled?(:first_class_vulnerabilities, default_enabled: true) not_found! unless Feature.enabled?(:first_class_vulnerabilities, default_enabled: true)
...@@ -63,7 +85,7 @@ module API ...@@ -63,7 +85,7 @@ module API
default: ::Vulnerabilities::Export.formats.each_key.first, default: ::Vulnerabilities::Export.formats.each_key.first,
values: ::Vulnerabilities::Export.formats.keys values: ::Vulnerabilities::Export.formats.keys
end end
desc 'Generate an instance level export' do desc 'Generate an instance-level export' do
success EE::API::Entities::VulnerabilityExport success EE::API::Entities::VulnerabilityExport
end end
post 'vulnerability_exports' do post 'vulnerability_exports' do
...@@ -73,7 +95,7 @@ module API ...@@ -73,7 +95,7 @@ module API
end end
end end
desc 'Get single project vulnerability export' do desc 'Get a single vulnerability export' do
success EE::API::Entities::VulnerabilityExport success EE::API::Entities::VulnerabilityExport
end end
get 'vulnerability_exports/:id' do get 'vulnerability_exports/:id' do
...@@ -88,7 +110,7 @@ module API ...@@ -88,7 +110,7 @@ module API
with: EE::API::Entities::VulnerabilityExport with: EE::API::Entities::VulnerabilityExport
end end
desc 'Download single project vulnerability export' desc 'Download a single vulnerability export'
get 'vulnerability_exports/:id/download' do get 'vulnerability_exports/:id/download' do
authorize! :read_vulnerability_export, vulnerability_export authorize! :read_vulnerability_export, vulnerability_export
......
...@@ -9,6 +9,7 @@ module EE ...@@ -9,6 +9,7 @@ module EE
expose :id expose :id
expose :created_at expose :created_at
expose :project_id expose :project_id
expose :group_id
expose :format expose :format
expose :status expose :status
expose :started_at expose :started_at
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
"id", "id",
"created_at", "created_at",
"project_id", "project_id",
"group_id",
"format", "format",
"status", "status",
"started_at", "started_at",
...@@ -13,7 +14,8 @@ ...@@ -13,7 +14,8 @@
"properties" : { "properties" : {
"id": { "type": "integer" }, "id": { "type": "integer" },
"created_at": { "type": "date" }, "created_at": { "type": "date" },
"project_id": { "type": "integer" }, "project_id": { "type": ["integer", "null"] },
"group_id": { "type": ["integer", "null"] },
"format": { "format": {
"type": "string", "type": "string",
"enum": ["csv"] "enum": ["csv"]
......
...@@ -14,6 +14,7 @@ describe ::EE::API::Entities::VulnerabilityExport do ...@@ -14,6 +14,7 @@ describe ::EE::API::Entities::VulnerabilityExport do
expect(subject[:id]).to eq(vulnerability_export.id) expect(subject[:id]).to eq(vulnerability_export.id)
expect(subject[:created_at]).to eq(vulnerability_export.created_at) expect(subject[:created_at]).to eq(vulnerability_export.created_at)
expect(subject[:project_id]).to eq(vulnerability_export.project_id) expect(subject[:project_id]).to eq(vulnerability_export.project_id)
expect(subject[:group_id]).to eq(vulnerability_export.group_id)
expect(subject[:format]).to eq(vulnerability_export.format) expect(subject[:format]).to eq(vulnerability_export.format)
expect(subject[:status]).to eq(vulnerability_export.status) expect(subject[:status]).to eq(vulnerability_export.status)
expect(subject[:started_at]).to eq(vulnerability_export.started_at) expect(subject[:started_at]).to eq(vulnerability_export.started_at)
......
...@@ -630,7 +630,9 @@ describe GroupPolicy do ...@@ -630,7 +630,9 @@ describe GroupPolicy do
end end
end end
describe 'read_group_security_dashboard' do describe 'read_group_security_dashboard & create_vulnerability_export' do
let(:abilities) { %i(read_group_security_dashboard create_vulnerability_export) }
before do before do
stub_licensed_features(security_dashboard: true) stub_licensed_features(security_dashboard: true)
end end
...@@ -638,57 +640,57 @@ describe GroupPolicy do ...@@ -638,57 +640,57 @@ describe GroupPolicy do
context 'with admin' do context 'with admin' do
let(:current_user) { admin } let(:current_user) { admin }
it { is_expected.to be_allowed(:read_group_security_dashboard) } it { is_expected.to be_allowed(*abilities) }
end end
context 'with owner' do context 'with owner' do
let(:current_user) { owner } let(:current_user) { owner }
it { is_expected.to be_allowed(:read_group_security_dashboard) } it { is_expected.to be_allowed(*abilities) }
end end
context 'with maintainer' do context 'with maintainer' do
let(:current_user) { maintainer } let(:current_user) { maintainer }
it { is_expected.to be_allowed(:read_group_security_dashboard) } it { is_expected.to be_allowed(*abilities) }
end end
context 'with developer' do context 'with developer' do
let(:current_user) { developer } let(:current_user) { developer }
it { is_expected.to be_allowed(:read_group_security_dashboard) } it { is_expected.to be_allowed(*abilities) }
context 'when security dashboard features is not available' do context 'when security dashboard features is not available' do
before do before do
stub_licensed_features(security_dashboard: false) stub_licensed_features(security_dashboard: false)
end end
it { is_expected.to be_disallowed(:read_group_security_dashboard) } it { is_expected.to be_disallowed(*abilities) }
end end
end end
context 'with reporter' do context 'with reporter' do
let(:current_user) { reporter } let(:current_user) { reporter }
it { is_expected.to be_disallowed(:read_group_security_dashboard) } it { is_expected.to be_disallowed(*abilities) }
end end
context 'with guest' do context 'with guest' do
let(:current_user) { guest } let(:current_user) { guest }
it { is_expected.to be_disallowed(:read_group_security_dashboard) } it { is_expected.to be_disallowed(*abilities) }
end end
context 'with non member' do context 'with non member' do
let(:current_user) { create(:user) } let(:current_user) { create(:user) }
it { is_expected.to be_disallowed(:read_group_security_dashboard) } it { is_expected.to be_disallowed(*abilities) }
end end
context 'with anonymous' do context 'with anonymous' do
let(:current_user) { nil } let(:current_user) { nil }
it { is_expected.to be_disallowed(:read_group_security_dashboard) } it { is_expected.to be_disallowed(*abilities) }
end end
end end
......
...@@ -71,6 +71,70 @@ describe API::VulnerabilityExports do ...@@ -71,6 +71,70 @@ describe API::VulnerabilityExports do
end end
end end
describe 'POST /security/groups/:id/vulnerability_exports' do
let_it_be(:group) { create(:group) }
let(:format) { 'csv' }
let(:request_path) { "/security/groups/#{group.id}/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
context 'when the user is not authorized to take the action' do
it 'responds with 403 forbidden' do
create_vulnerability_export
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when the user is authorized to take the action' 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)
group.add_developer(user)
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
end
it_behaves_like 'forbids access to vulnerability API endpoint in case of disabled features'
end
describe 'POST /security/vulnerability_exports' do describe 'POST /security/vulnerability_exports' do
let(:format) { 'csv' } let(:format) { 'csv' }
let(:request_path) { "/security/vulnerability_exports" } let(:request_path) { "/security/vulnerability_exports" }
......
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