Commit 4bdc6aac authored by manojmj's avatar manojmj

Add API endpoints for soft-delete/restore of groups

This change adds API endpoints to perfrom
1. soft-deleting of group
2. restoring of already soft-deleted groups.
parent f7be1aee
---
title: Add API endpoints for 'soft-delete for groups' feature
merge_request: 19430
author:
type: added
...@@ -628,7 +628,12 @@ Feature.disable(:limit_projects_in_groups_api) ...@@ -628,7 +628,12 @@ Feature.disable(:limit_projects_in_groups_api)
## Remove group ## Remove group
Removes group with all projects inside. Only available to group owners and administrators. Only available to group owners and administrators.
This endpoint either:
- Removes group, and queues a background job to delete all projects in the group as well.
- Since GitLab 12.8, on [Premium](https://about.gitlab.com/pricing/premium/) or higher tiers, marks a group for deletion. The deletion will happen 7 days later by default, but this can be changed in the [instance settings](../user/admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only).
``` ```
DELETE /groups/:id DELETE /groups/:id
...@@ -636,10 +641,27 @@ DELETE /groups/:id ...@@ -636,10 +641,27 @@ DELETE /groups/:id
Parameters: Parameters:
- `id` (required) - The ID or path of a user group | Attribute | Type | Required | Description |
| --------------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
The response will be `202 Accepted` if the user has authorization.
## Restore group marked for deletion **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/33257) in GitLab 12.8.
This will queue a background job to delete all projects in the group. The Restores a group marked for deletion.
response will be a 202 Accepted if the user has authorization.
```plaintext
POST /groups/:id/restore
```
Parameters:
| Attribute | Type | Required | Description |
| --------------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
## Search for group ## Search for group
......
...@@ -1769,7 +1769,7 @@ This endpoint either: ...@@ -1769,7 +1769,7 @@ This endpoint either:
- Removes a project including all associated resources (issues, merge requests etc). - Removes a project including all associated resources (issues, merge requests etc).
- From GitLab 12.6 on Premium or higher tiers, marks a project for deletion. Actual - From GitLab 12.6 on Premium or higher tiers, marks a project for deletion. Actual
deletion happens after number of days specified in deletion happens after number of days specified in
[instance settings](../user/admin_area/settings/visibility_and_access_controls.md#project-deletion-adjourned-period-premium-only). [instance settings](../user/admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only).
``` ```
DELETE /projects/:id DELETE /projects/:id
...@@ -1781,6 +1781,8 @@ DELETE /projects/:id ...@@ -1781,6 +1781,8 @@ DELETE /projects/:id
## Restore project marked for deletion **(PREMIUM)** ## Restore project marked for deletion **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/32935) in GitLab 12.6.
Restores project marked for deletion. Restores project marked for deletion.
``` ```
......
...@@ -294,7 +294,7 @@ are listed in the descriptions of the relevant settings. ...@@ -294,7 +294,7 @@ are listed in the descriptions of the relevant settings.
| `plantuml_enabled` | boolean | no | (**If enabled, requires:** `plantuml_url`) Enable PlantUML integration. Default is `false`. | | `plantuml_enabled` | boolean | no | (**If enabled, requires:** `plantuml_url`) Enable PlantUML integration. Default is `false`. |
| `plantuml_url` | string | required by: `plantuml_enabled` | The PlantUML instance URL for integration. | | `plantuml_url` | string | required by: `plantuml_enabled` | The PlantUML instance URL for integration. |
| `polling_interval_multiplier` | decimal | no | Interval multiplier used by endpoints that perform polling. Set to `0` to disable polling. | | `polling_interval_multiplier` | decimal | no | Interval multiplier used by endpoints that perform polling. Set to `0` to disable polling. |
| `deletion_adjourned_period` | integer | no | **(PREMIUM ONLY)** How many days after marking project for deletion it is actually removed. Value between 0 and 90. | `deletion_adjourned_period` | integer | no | **(PREMIUM ONLY)** The number of days to wait before removing a project or group that is marked for deletion. Value must be between 0 and 90.
| `project_export_enabled` | boolean | no | Enable project export. | | `project_export_enabled` | boolean | no | Enable project export. |
| `prometheus_metrics_enabled` | boolean | no | Enable Prometheus metrics. | | `prometheus_metrics_enabled` | boolean | no | Enable Prometheus metrics. |
| `protected_ci_variables` | boolean | no | Environment variables are protected by default. | | `protected_ci_variables` | boolean | no | Environment variables are protected by default. |
......
...@@ -47,11 +47,13 @@ To ensure only admin users can delete projects: ...@@ -47,11 +47,13 @@ To ensure only admin users can delete projects:
1. Check the **Default project deletion protection** checkbox. 1. Check the **Default project deletion protection** checkbox.
1. Click **Save changes**. 1. Click **Save changes**.
## Project deletion adjourned period **(PREMIUM ONLY)** ## Default deletion adjourned period **(PREMIUM ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/32935) in GitLab 12.6. > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/32935) in GitLab 12.6.
By default, project marked for deletion will be permanently removed after 7 days. This period may be changed. By default, a project or group marked for removal will be permanently removed after 7 days.
This period may be changed, and setting this period to 0 will enable immediate removal
of projects or groups.
To change this period: To change this period:
......
...@@ -279,6 +279,12 @@ module EE ...@@ -279,6 +279,12 @@ module EE
marked_for_deletion_on.present? marked_for_deletion_on.present?
end end
def adjourned_deletion?
return false unless feature_available?(:adjourned_deletion_for_projects_and_groups)
::Gitlab::CurrentSettings.deletion_adjourned_period > 0
end
private private
def custom_project_templates_group_allowed def custom_project_templates_group_allowed
......
...@@ -66,6 +66,7 @@ module EE ...@@ -66,6 +66,7 @@ module EE
expose :checked_file_template_project_id, expose :checked_file_template_project_id,
as: :file_template_project_id, as: :file_template_project_id,
if: ->(group, options) { group.feature_available?(:custom_file_templates_for_namespace) } if: ->(group, options) { group.feature_available?(:custom_file_templates_for_namespace) }
expose :marked_for_deletion_on, if: ->(group, _) { group.feature_available?(:adjourned_deletion_for_projects_and_groups) }
end end
end end
......
...@@ -12,7 +12,7 @@ module EE ...@@ -12,7 +12,7 @@ module EE
override :find_groups override :find_groups
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def find_groups(params, parent_id = nil) def find_groups(params, parent_id = nil)
super.preload(:ldap_group_links) super.preload(:ldap_group_links, :deletion_schedule)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
...@@ -54,6 +54,21 @@ module EE ...@@ -54,6 +54,21 @@ module EE
audit_log_finder_params = params.slice(:created_after, :created_before) audit_log_finder_params = params.slice(:created_after, :created_before)
audit_log_finder_params.merge(entity_type: group.class.name, entity_id: group.id) audit_log_finder_params.merge(entity_type: group.class.name, entity_id: group.id)
end end
override :delete_group
def delete_group(group)
return super unless group.adjourned_deletion?
result = destroy_conditionally!(group) do |group|
::Groups::MarkForDeletionService.new(group, current_user).execute
end
if result[:status] == :success
accepted!
else
render_api_error!(result[:message], 400)
end
end
end end
resource :groups, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource :groups, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
...@@ -104,6 +119,20 @@ module EE ...@@ -104,6 +119,20 @@ module EE
present audit_event, with: EE::API::Entities::AuditEvent present audit_event, with: EE::API::Entities::AuditEvent
end end
end end
desc 'Restore a group.'
post ':id/restore' do
authorize! :admin_group, user_group
break not_found! unless user_group.feature_available?(:adjourned_deletion_for_projects_and_groups)
result = ::Groups::RestoreService.new(user_group, current_user).execute
if result[:status] == :success
present user_group, with: ::API::Entities::GroupDetail, current_user: current_user
else
render_api_error!(result[:message], 400)
end
end
end end
end end
end end
......
...@@ -639,6 +639,46 @@ describe Group do ...@@ -639,6 +639,46 @@ describe Group do
describe '#marked_for_deletion?' do describe '#marked_for_deletion?' do
subject { group.marked_for_deletion? } subject { group.marked_for_deletion? }
context 'adjourned deletion feature is available' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: true)
end
context 'when the group is marked for adjourned deletion' do
before do
create(:group_deletion_schedule, group: group, marked_for_deletion_on: 1.day.ago)
end
it { is_expected.to be_truthy }
end
context 'when the group is not marked for adjourned deletion' do
it { is_expected.to be_falsey }
end
end
context 'adjourned deletion feature is not available' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: false)
end
context 'when the group is marked for adjourned deletion' do
before do
create(:group_deletion_schedule, group: group, marked_for_deletion_on: 1.day.ago)
end
it { is_expected.to be_falsey }
end
context 'when the group is not marked for adjourned deletion' do
it { is_expected.to be_falsey }
end
end
end
describe '#adjourned_deletion?' do
subject { group.adjourned_deletion? }
shared_examples_for 'returns false' do shared_examples_for 'returns false' do
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
end end
...@@ -652,15 +692,19 @@ describe Group do ...@@ -652,15 +692,19 @@ describe Group do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: true) stub_licensed_features(adjourned_deletion_for_projects_and_groups: true)
end end
context 'when the group is marked for adjourned deletion' do context 'when adjourned deletion period is set to more than 0' do
before do before do
create(:group_deletion_schedule, group: group, marked_for_deletion_on: 1.day.ago) stub_application_setting(deletion_adjourned_period: 1)
end end
it_behaves_like 'returns true' it_behaves_like 'returns true'
end end
context 'when the group is not marked for adjourned deletion' do context 'when adjourned deletion period is set to 0' do
before do
stub_application_setting(deletion_adjourned_period: 0)
end
it_behaves_like 'returns false' it_behaves_like 'returns false'
end end
end end
...@@ -670,15 +714,19 @@ describe Group do ...@@ -670,15 +714,19 @@ describe Group do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: false) stub_licensed_features(adjourned_deletion_for_projects_and_groups: false)
end end
context 'when the group is marked for adjourned deletion' do context 'when adjourned deletion period is set to more than 0' do
before do before do
create(:group_deletion_schedule, group: group, marked_for_deletion_on: 1.day.ago) stub_application_setting(deletion_adjourned_period: 1)
end end
it_behaves_like 'returns false' it_behaves_like 'returns false'
end end
context 'when the group is not marked for adjourned deletion' do context 'when adjourned deletion period is set to 0' do
before do
stub_application_setting(deletion_adjourned_period: 0)
end
it_behaves_like 'returns false' it_behaves_like 'returns false'
end end
end end
......
...@@ -44,6 +44,7 @@ describe API::Groups do ...@@ -44,6 +44,7 @@ describe API::Groups do
end end
describe 'GET /groups/:id' do describe 'GET /groups/:id' do
context 'group_ip_restriction' do
before do before do
create(:ip_restriction, group: private_group) create(:ip_restriction, group: private_group)
private_group.add_maintainer(user) private_group.add_maintainer(user)
...@@ -80,6 +81,33 @@ describe API::Groups do ...@@ -80,6 +81,33 @@ describe API::Groups do
end end
end end
context 'marked_for_deletion_on attribute' do
context 'when feature is available' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: true)
end
it 'is exposed' do
get api("/groups/#{group.id}", user)
expect(json_response).to have_key 'marked_for_deletion_on'
end
end
context 'when feature is not available' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: false)
end
it 'is not exposed' do
get api("/groups/#{group.id}", user)
expect(json_response).not_to have_key 'marked_for_deletion_on'
end
end
end
end
describe 'PUT /groups/:id' do describe 'PUT /groups/:id' do
subject { put api("/groups/#{group.id}", user), params: params } subject { put api("/groups/#{group.id}", user), params: params }
...@@ -460,6 +488,138 @@ describe API::Groups do ...@@ -460,6 +488,138 @@ describe API::Groups do
end end
end end
describe "DELETE /groups/:id" do
subject { delete api("/groups/#{group.id}", user) }
shared_examples_for 'immediately enqueues the job to delete the group' do
it do
Sidekiq::Testing.fake! do
expect { subject }.to change(GroupDestroyWorker.jobs, :size).by(1)
end
expect(response).to have_gitlab_http_status(202)
end
end
context 'feature is available' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: true)
end
context 'period for adjourned deletion is greater than 0' do
before do
stub_application_setting(deletion_adjourned_period: 1)
end
context 'success' do
it 'marks the group for adjourned deletion' do
subject
group.reload
expect(response).to have_gitlab_http_status(202)
expect(group.marked_for_deletion_on).to eq(Date.today)
expect(group.deleting_user).to eq(user)
end
it 'does not immediately enqueue the job to delete the group' do
expect { subject }.not_to change(GroupDestroyWorker.jobs, :size)
end
end
context 'failure' do
before do
allow(::Groups::MarkForDeletionService).to receive_message_chain(:new, :execute).and_return({ status: :error, message: 'error' })
end
it 'returns error' do
subject
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('error')
end
end
end
context 'period of adjourned deletion is set to 0' do
before do
stub_application_setting(deletion_adjourned_period: 0)
end
it_behaves_like 'immediately enqueues the job to delete the group'
end
end
context 'feature is not available' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: false)
end
it_behaves_like 'immediately enqueues the job to delete the group'
end
end
describe "POST /groups/:id/restore" do
let(:group) do
create(:group_with_deletion_schedule,
marked_for_deletion_on: 1.day.ago,
deleting_user: user)
end
subject { post api("/groups/#{group.id}/restore", user) }
context 'feature is available' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: true)
end
context 'authenticated as owner' do
context 'restoring is successful' do
it 'restores the group to original state' do
subject
expect(response).to have_gitlab_http_status(201)
expect(json_response['marked_for_deletion_on']).to be_falsey
end
end
context 'restoring fails' do
before do
allow(::Groups::RestoreService).to receive_message_chain(:new, :execute).and_return({ status: :error, message: 'error' })
end
it 'returns error' do
subject
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('error')
end
end
end
context 'authenticated as user without access to the group' do
subject { post api("/groups/#{group.id}/restore", another_user) }
it 'returns 403' do
subject
expect(response).to have_gitlab_http_status(403)
end
end
end
context 'feature is not available' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: false)
end
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
end
def ldap_sync(group_id, user, sidekiq_testing_method) def ldap_sync(group_id, user, sidekiq_testing_method)
Sidekiq::Testing.send(sidekiq_testing_method) do Sidekiq::Testing.send(sidekiq_testing_method) do
post api("/groups/#{group_id}/ldap_sync", user) post api("/groups/#{group_id}/ldap_sync", user)
......
...@@ -92,6 +92,15 @@ module API ...@@ -92,6 +92,15 @@ module API
present paginate(groups), options present paginate(groups), options
end end
def delete_group(group)
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/46285')
destroy_conditionally!(group) do |group|
::Groups::DestroyService.new(group, current_user).async_execute
end
accepted!
end
end end
resource :groups do resource :groups do
...@@ -187,12 +196,7 @@ module API ...@@ -187,12 +196,7 @@ module API
group = find_group!(params[:id]) group = find_group!(params[:id])
authorize! :admin_group, group authorize! :admin_group, group
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/46285') delete_group(group)
destroy_conditionally!(group) do |group|
::Groups::DestroyService.new(group, current_user).async_execute
end
accepted!
end end
desc 'Get a list of projects in this group.' do desc 'Get a list of projects in this group.' 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