Commit ae7a45cd authored by Gosia Ksionek's avatar Gosia Ksionek Committed by Imre Farkas

Add application settings

Add new columns to projects and application settings

Add attributes to import specs

Import should ignore marked for deletion column

Remove unneeded differences

Remove unneeded differences

Add specs to model

Refactor specs to use specs helper

Add cr remarks

Add cr remarks

Add db changes

Add new columns to projects and application settings

Add attributes to import specs

Import should ignore marked for deletion column

Add application setting

With api entry and ui changes

Fix validations name

Remove unneeded differences

Add strings to the file

Add changelog entry

Change name in migration to the proper one

Remove unneeded differences

Remove parts of code that by error was brought back to old version

Add constant to application settings

Add constant to migration

Correct wording for more universal approach

Update changelog entry

Remove merge leftovers

Remove rubocop offence
parent e13a21fe
<script>
import icon from '~/vue_shared/components/icon.vue';
import { GlBadge } from '@gitlab/ui';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import {
ITEM_TYPE,
......@@ -8,13 +9,16 @@ import {
PROJECT_VISIBILITY_TYPE,
} from '../constants';
import itemStatsValue from './item_stats_value.vue';
import isProjectPendingRemoval from 'ee_else_ce/groups/mixins/is_project_pending_removal';
export default {
components: {
icon,
timeAgoTooltip,
itemStatsValue,
GlBadge,
},
mixins: [isProjectPendingRemoval],
props: {
item: {
type: Object,
......@@ -70,6 +74,9 @@ export default {
css-class="project-stars"
icon-name="star"
/>
<div v-if="isProjectPendingRemoval">
<gl-badge variant="warning">{{ __('pending removal') }}</gl-badge>
</div>
<div v-if="isProject" class="last-updated">
<time-ago-tooltip :time="item.updatedAt" tooltip-placement="bottom" />
</div>
......
export default {
computed: {
isProjectPendingRemoval() {
return false;
},
},
};
......@@ -93,6 +93,7 @@ export default class GroupsStore {
memberCount: rawGroupItem.number_users_with_delimiter,
starCount: rawGroupItem.star_count,
updatedAt: rawGroupItem.updated_at,
pendingRemoval: rawGroupItem.marked_for_deletion_at,
};
}
......
......@@ -99,3 +99,5 @@ class GroupChildEntity < Grape::Entity
end
end
end
GroupChildEntity.prepend_if_ee('EE::GroupChildEntity')
......@@ -9,6 +9,7 @@
= f.label s_('ProjectCreationLevel|Default project creation protection'), class: 'label-bold'
= f.select :default_project_creation, options_for_select(Gitlab::Access.project_creation_options, @application_setting.default_project_creation), {}, class: 'form-control'
= render_if_exists 'admin/application_settings/default_project_deletion_protection_setting', form: f
= render_if_exists 'admin/application_settings/default_project_deletion_adjourned_period_setting', form: f
.form-group.visibility-level-setting
= f.label :default_project_visibility, class: 'label-bold'
= render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new)
......@@ -53,6 +54,7 @@
= select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
%span.form-text.text-muted#clone-protocol-help
= _('Allow only the selected protocols to be used for Git access.')
.form-group
= f.label :custom_http_clone_url_root, _('Custom Git clone URL for HTTP(S)'), class: 'label-bold'
= f.text_field :custom_http_clone_url_root, class: 'form-control', placeholder: 'https://git.example.com', :'aria-describedby' => 'custom_http_clone_url_root_help_block'
......
- if project.archived
%span.badge.badge-warning
= _('archived')
......@@ -14,8 +14,7 @@
.stats
%span.badge.badge-pill
= storage_counter(project.statistics&.storage_size)
- if project.archived
%span.badge.badge-warning archived
= render_if_exists 'admin/projects/archived', project: project
.title
= link_to(admin_project_path(project)) do
.dash-project-avatar
......
- if project.archived?
.text-warning.center.prepend-top-20
%p
= icon("exclamation-triangle fw")
= _('Archived project! Repository and other project resources are read only')
- return unless can?(current_user, :remove_project, project)
.sub-section
%h4.danger-title= _('Remove project')
%p
%strong= _('Removing the project will delete its repository and all related resources including issues, merge requests etc.')
= form_tag(project_path(project), method: :delete) do
%p
%strong= _('Removed projects cannot be restored!')
= button_to _('Remove project'), '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(project) }
......@@ -73,23 +73,7 @@
= render 'export', project: @project
- if can? current_user, :archive_project, @project
.sub-section
%h4.warning-title
- if @project.archived?
= _('Unarchive project')
- else
= _('Archive project')
- if @project.archived?
%p= _("Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments and other entities can be created. <strong>Once active this project shows up in the search and on the dashboard.</strong>").html_safe
= link_to _('Unarchive project'), unarchive_project_path(@project),
data: { confirm: _("Are you sure that you want to unarchive this project?"), qa_selector: 'unarchive_project_link' },
method: :post, class: "btn btn-success"
- else
%p= _("Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches. <strong>The repository cannot be committed to, and no issues, comments or other entities can be created.</strong>").html_safe
= link_to _('Archive project'), archive_project_path(@project),
data: { confirm: _("Are you sure that you want to archive this project?"), qa_selector: 'archive_project_link' },
method: :post, class: "btn btn-warning"
= render_if_exists 'projects/settings/archive'
.sub-section.rename-repository
%h4.warning-title= _('Change path')
= render 'projects/errors'
......@@ -135,14 +119,7 @@
%strong= _('Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source.')
= button_to _('Remove fork relationship'), '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_warning_message(@project) }
- if can?(current_user, :remove_project, @project)
.sub-section
%h4.danger-title= _('Remove project')
%p= _('Removing the project will delete its repository and all related resources including issues, merge requests etc.')
= form_tag(project_path(@project), method: :delete) do
%p
%strong= _('Removed projects cannot be restored!')
= button_to _('Remove project'), '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) }
= render 'remove', project: @project
.save-project-loader.hide
.center
......
- return unless can?(current_user, :archive_project, @project)
.sub-section
%h4.warning-title
- if @project.archived?
= _('Unarchive project')
- else
= _('Archive project')
- if @project.archived?
%p= _("Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
= link_to _('Unarchive project'), unarchive_project_path(@project),
data: { confirm: _("Are you sure that you want to unarchive this project?"), qa_selector: 'unarchive_project_link' },
method: :post, class: "btn btn-success"
- else
%p= _("Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
= link_to _('Archive project'), archive_project_path(@project),
data: { confirm: _("Are you sure that you want to archive this project?"), qa_selector: 'archive_project_link' },
method: :post, class: "btn btn-warning"
......@@ -18,11 +18,8 @@
- if can?(current_user, :download_code, @project) && @project.repository_languages.present?
= repository_languages_bar(@project.repository_languages)
- if @project.archived?
.text-warning.center.prepend-top-20
%p
= icon("exclamation-triangle fw")
#{ _('Archived project! Repository and other project resources are read-only') }
= render "archived_notice", project: @project
= render_if_exists "projects/marked_for_deletion_notice", project: @project
- view_path = @project.default_view
......
- if project.archived
%span.d-flex.badge.badge-warning
= _('archived')
......@@ -67,8 +67,7 @@
%span.icon-wrapper.pipeline-status
= render 'ci/status/icon', status: project.last_pipeline.detailed_status(current_user), tooltip_placement: 'top', path: pipeline_path
- if project.archived
%span.d-flex.icon-wrapper.badge.badge-warning archived
= render_if_exists 'shared/projects/archived', project: project
- if stars
= link_to project_starrers_path(project),
class: "d-flex align-items-center icon-wrapper stars has-tooltip",
......
......@@ -475,6 +475,9 @@ Gitlab.ee do
Settings.cron_jobs['clear_shared_runners_minutes_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['clear_shared_runners_minutes_worker']['cron'] ||= '0 0 1 * *'
Settings.cron_jobs['clear_shared_runners_minutes_worker']['job_class'] = 'ClearSharedRunnersMinutesWorker'
Settings.cron_jobs['adjourned_projects_deletion_cron_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['adjourned_projects_deletion_cron_worker']['cron'] ||= '0 4 * * *'
Settings.cron_jobs['adjourned_projects_deletion_cron_worker']['job_class'] = 'AdjournedProjectsDeletionCronWorker'
Settings.cron_jobs['geo_file_download_dispatch_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['geo_file_download_dispatch_worker']['cron'] ||= '*/1 * * * *'
Settings.cron_jobs['geo_file_download_dispatch_worker']['job_class'] ||= 'Geo::FileDownloadDispatchWorker'
......
......@@ -124,4 +124,4 @@
- [design_management_new_version, 1]
- [epics, 2]
- [personal_access_tokens, 1]
- [adjourned_project_deletion, 1]
......@@ -1713,7 +1713,12 @@ Example response:
## Remove project
Removes a project including all associated resources (issues, merge requests etc).
This endpoint either:
- 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
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).
```
DELETE /projects/:id
......@@ -1723,6 +1728,18 @@ DELETE /projects/:id
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
## Restore project marked for deletion **(PREMIUM)**
Restores project marked for deletion.
```
POST /projects/:id/restore
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
## Upload a file
Uploads a file to the specified project to be used in an issue or merge request description, or a comment.
......
......@@ -72,14 +72,15 @@ Example response:
```
Users on GitLab [Premium or Ultimate](https://about.gitlab.com/pricing/) may also see
the `file_template_project_id` or the `geo_node_allowed_ips` parameters:
the `file_template_project_id`, `deletion_adjourned_period`, or the `geo_node_allowed_ips` parameters:
```json
{
"id" : 1,
"signup_enabled" : true,
"file_template_project_id": 1,
"geo_node_allowed_ips": "0.0.0.0/0, ::/0"
"geo_node_allowed_ips": "0.0.0.0/0, ::/0",
"deletion_adjourned_period": 7,
...
}
```
......@@ -162,6 +163,7 @@ these parameters:
- `file_template_project_id`
- `geo_node_allowed_ips`
- `geo_status_timeout`
- `deletion_adjourned_period`
Example responses: **(PREMIUM ONLY)**
......@@ -292,6 +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_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. |
| `deletion_adjourned_period` | integer | no | **(PREMIUM ONLY)** How many days after marking project for deletion it is actually removed. Value between 0 and 90.
| `project_export_enabled` | boolean | no | Enable project export. |
| `prometheus_metrics_enabled` | boolean | no | Enable Prometheus metrics. |
| `protected_ci_variables` | boolean | no | Environment variables are protected by default. |
......
......@@ -48,6 +48,17 @@ To ensure only admin users can delete projects:
1. Check the **Default project deletion protection** checkbox.
1. Click **Save changes**.
## Project deletion adjourned period **(PREMIUM ONLY)**
> [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.
To change this period:
1. Select the desired option.
1. Click **Save changes**.
## Default project visibility
To set the default visibility levels for new projects:
......
import { ITEM_TYPE } from '~/groups/constants';
export default {
computed: {
isProjectPendingRemoval() {
return this.item.type === ITEM_TYPE.PROJECT && this.item.pendingRemoval;
},
},
};
......@@ -38,6 +38,10 @@ module EE
attrs << :default_project_deletion_protection
end
if License.feature_available?(:marking_project_for_deletion)
attrs << :deletion_adjourned_period
end
if License.feature_available?(:required_ci_templates)
attrs << :required_instance_ci_template
end
......
......@@ -11,6 +11,40 @@ module EE
before_action :log_unarchive_audit_event, only: [:unarchive]
end
def restore
return access_denied! unless can?(current_user, :remove_project, project)
result = ::Projects::RestoreService.new(project, current_user, {}).execute
if result[:status] == :success
flash[:notice] = _("Project '%{project_name}' is restored.") % { project_name: project.full_name }
redirect_to(edit_project_path(project))
else
flash.now[:alert] = result[:message]
render 'edit'
end
end
override :destroy
def destroy
return super unless project.adjourned_deletion?
return access_denied! unless can?(current_user, :remove_project, project)
result = ::Projects::MarkForDeletionService.new(project, current_user, {}).execute
if result[:status] == :success
date = permanent_deletion_date(project.marked_for_deletion_at)
flash[:notice] = _("Project '%{project_name}' will be deleted on %{date}") % { date: date, project_name: project.full_name }
redirect_to(project_path(project), status: :found)
else
flash.now[:alert] = result[:message]
render 'edit'
end
end
override :project_params_attributes
def project_params_attributes
super + project_params_ee
......
......@@ -88,6 +88,7 @@ module EE
email_additional_text
file_template_project_id
default_project_deletion_protection
deletion_adjourned_period
]
end
end
......
......@@ -116,6 +116,19 @@ module EE
super || project_feature_flags_path(project)
end
override :remove_project_message
def remove_project_message(project)
return super unless project.feature_available?(:marking_project_for_deletion)
date = permanent_deletion_date(Time.now.utc)
_("Removing a project places it into a read-only state until %{date}, at which point the project will be permanantly removed. Are you ABSOLUTELY sure?") %
{ date: date }
end
def permanent_deletion_date(date)
(date + ::Gitlab::CurrentSettings.deletion_adjourned_period.days).strftime('%F')
end
# Given the current GitLab configuration, check whether the GitLab URL for Kerberos is going to be different than the HTTP URL
def alternative_kerberos_url?
::Gitlab.config.alternative_gitlab_kerberos_url?
......
......@@ -11,6 +11,7 @@ module EE
prepended do
EMAIL_ADDITIONAL_TEXT_CHARACTER_LIMIT = 10_000
INSTANCE_REVIEW_MIN_USERS = 100
DEFAULT_NUMBER_OF_DAYS_BEFORE_REMOVAL = 7
belongs_to :file_template_project, class_name: "Project"
......@@ -39,6 +40,10 @@ module EE
presence: true,
numericality: { only_integer: true, greater_than: 0 }
validates :deletion_adjourned_period,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 90 }
validates :elasticsearch_replicas,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
......@@ -87,6 +92,7 @@ module EE
mirror_capacity_threshold: Settings.gitlab['mirror_capacity_threshold'],
mirror_max_capacity: Settings.gitlab['mirror_max_capacity'],
mirror_max_delay: Settings.gitlab['mirror_max_delay'],
deletion_adjourned_period: DEFAULT_NUMBER_OF_DAYS_BEFORE_REMOVAL,
pseudonymizer_enabled: false,
repository_size_limit: 0,
slack_app_enabled: false,
......
# frozen_string_literal: true
module EE
module GroupChildEntity
extend ActiveSupport::Concern
prepended do
# Project only attributes
expose :marked_for_deletion_at,
if: lambda { |_instance, _options| project? }
end
end
end
# frozen_string_literal: true
module Projects
class MarkForDeletionService < BaseService
def execute
return if project.marked_for_deletion_at?
return unless project.feature_available?(:marking_project_for_deletion)
result = ::Projects::UpdateService.new(
project,
current_user,
{ archived: true,
marked_for_deletion_at: Time.now.utc,
deleting_user: current_user }
).execute
log_event if result[:status] == :success
log_error(result[:message]) if result[:status] == :error
result
end
def log_event
log_audit_event
log_info("User #{current_user.id} marked project #{project.full_path} for deletion")
end
def log_audit_event
::AuditEventService.new(
current_user,
project,
action: :custom,
custom_message: "Project marked for deletion"
).for_project.security_event
end
end
end
# frozen_string_literal: true
module Projects
class RestoreService < BaseService
def execute
return error(_('Project already deleted')) if project.pending_delete?
result = ::Projects::UpdateService.new(
project,
current_user,
{ archived: false,
marked_for_deletion_at: nil,
deleting_user: nil }
).execute
log_event if result[:status] == :success
result
end
def log_event
log_audit_event
log_info("User #{current_user.id} restored project #{project.full_path}")
end
def log_audit_event
::AuditEventService.new(
current_user,
project,
action: :custom,
custom_message: "Project restored"
).for_project.security_event
end
end
end
- return unless License.feature_available?(:marking_project_for_deletion)
- f = local_assigns.fetch(:form)
.form-group
= f.label s_('Default deletion adjourned period'), class: 'label-bold'
= f.select :deletion_adjourned_period, options_for_select(0..90, @application_setting.deletion_adjourned_period), {}, class: 'form-control'
= f.label :deletion_adjourned_period, class: 'form-check-label' do
= _('How many days need to pass between marking entity for deletion and actual removing it.')
- if project.marked_for_deletion?
%span.badge.badge-warning
= _('pending removal')
- elsif project.archived
%span.badge.badge-warning
= _('archived')
- return if project.marked_for_deletion?
- if project.archived?
.text-warning.center.prepend-top-20
%p
= icon("exclamation-triangle fw")
= _('Archived project! Repository and other project resources are read-only')
- if project.marked_for_deletion?
.text-warning.center.prepend-top-20
%p
= icon("exclamation-triangle fw")
= _("Deletion pending. This project will be removed on %{date}. Repository and other project resources are read-only.") % { date: permanent_deletion_date(project.marked_for_deletion_at) }
- return unless can?(current_user, :remove_project, project)
- unless project.marked_for_deletion?
.sub-section
%h4.danger-title= _('Remove project')
= render 'projects/settings/marked_for_removal'
%p
%strong= _('Removing the project will delete its repository and all related resources including issues, merge requests etc.')
= form_tag(project_path(project), method: :delete) do
%p
%strong= _('Removed projects cannot be restored!')
= button_to _('Remove project'), '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(project) }
- else
= render 'projects/settings/restore', project: project
- return if @project.marked_for_deletion?
= render_ce 'projects/settings/archive'
- return unless @project.feature_available?(:marking_project_for_deletion)
- date = permanent_deletion_date(Time.now.utc)
%p
= _("Removing a project places it into a read-only state until %{date}, at which point the project will be permanently removed.") %{ date: date }
%br
= _("Until that time, the project can be restored.")
- return unless project.feature_available?(:marking_project_for_deletion)
- date = permanent_deletion_date(project.marked_for_deletion_at)
.sub-section
%h4.danger-title= _('Restore project')
%p
%strong= _('This project will be removed on %{date}') %{ date: date }
%p
= _("Restoring the project will prevent the project from being removed on this date and restore people's ability to make changes to it.")
= _("The repository can be commited to, and issues, comments and other entities can be created.")
%strong= _('Only active this projects shows up in the search and on the dashboard.')
= link_to _('Restore project'), namespace_project_restore_path(project.namespace, project),
method: :post, class: "btn btn-remove"
- if project.marked_for_deletion?
%span.d-flex.badge.badge-warning
= _('pending removal')
- elsif project.archived
%span.d-flex.badge.badge-warning
= _('archived')
# frozen_string_literal: true
class AdjournedProjectDeletionWorker
include ApplicationWorker
include ExceptionBacktrace
feature_category :authentication_and_authorization
def perform(project_id)
project = Project.find(project_id)
user = project.deleting_user
return unless user
::Projects::DestroyService.new(project, user).async_execute
rescue ActiveRecord::RecordNotFound => error
logger.error("Failed to delete project (#{project_id}): #{error.message}")
end
end
# frozen_string_literal: true
class AdjournedProjectsDeletionCronWorker
include ApplicationWorker
include CronjobQueue
INTERVAL = 5.minutes.to_i
feature_category :authentication_and_authorization
def perform
deletion_cutoff = Gitlab::CurrentSettings.deletion_adjourned_period.days.ago.to_date
Project.aimed_for_deletion(deletion_cutoff).find_each(batch_size: 100).with_index do |project, index| # rubocop: disable CodeReuse/ActiveRecord
delay = index * INTERVAL
AdjournedProjectDeletionWorker.perform_in(delay, project.id)
end
end
end
......@@ -21,6 +21,7 @@
- cronjob:update_all_mirrors
- cronjob:pseudonymizer
- cronjob:update_max_seats_used_for_gitlab_com_subscriptions
- cronjob:adjourned_projects_deletion_cron
- gcp_cluster:cluster_update_app
- gcp_cluster:cluster_wait_for_app_update
......@@ -74,6 +75,7 @@
- new_epic
- project_import_schedule
- project_update_repository_storage
- adjourned_project_deletion
- rebase
- refresh_license_compliance_checks
- repository_update_mirror
......
---
title: Add application settings needed for soft-deletion
merge_request: 18790
author:
type: added
......@@ -113,6 +113,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
end
post '/restore' => '/projects#restore', as: :restore
end
# End of the /-/ scope.
......
......@@ -50,6 +50,7 @@ module EE
expose :packages_enabled, if: ->(project, _) { project.feature_available?(:packages) }
expose :service_desk_enabled, if: ->(project, _) { project.feature_available?(:service_desk) }
expose :service_desk_address, if: ->(project, _) { project.feature_available?(:service_desk) }
expose :marked_for_deletion_at, if: ->(project, _) { project.feature_available?(:marking_project_for_deletion) }
end
end
......@@ -202,6 +203,7 @@ module EE
expose :email_additional_text, if: ->(_instance, _opts) { ::License.feature_available?(:email_additional_text) }
expose :file_template_project_id, if: ->(_instance, _opts) { ::License.feature_available?(:custom_file_templates) }
expose :default_project_deletion_protection, if: ->(_instance, _opts) { ::License.feature_available?(:default_project_deletion_protection) }
expose :deletion_adjourned_period, if: ->(_instance, _opts) { ::License.feature_available?(:marking_project_for_deletion) }
end
end
......
......@@ -31,6 +31,7 @@ module EE
optional :email_additional_text, type: String, desc: 'Additional text added to the bottom of every email for legal/auditing/compliance reasons'
optional :default_project_deletion_protection, type: Grape::API::Boolean, desc: 'Disable project owners ability to delete project'
optional :deletion_adjourned_period, type: Integer, desc: 'Number of days between marking project as deleted and actual removal'
optional :help_text, type: String, desc: 'GitLab server administrator information'
optional :repository_size_limit, type: Integer, desc: 'Size limit per repository (MB)'
optional :file_template_project_id, type: Integer, desc: 'ID of project where instance-level file templates are stored.'
......
......@@ -6,6 +6,23 @@ module EE
extend ActiveSupport::Concern
prepended do
resource :projects do
desc 'Restore a project' do
success Entities::Project
end
post ':id/restore' do
authorize!(:remove_project, user_project)
break not_found! unless user_project.feature_available?(:marking_project_for_deletion)
result = ::Projects::RestoreService.new(user_project, current_user).execute
if result[:status] == :success
present user_project, with: ::API::Entities::Project, current_user: current_user
else
render_api_error!(result[:message], 400)
end
end
end
helpers do
extend ::Gitlab::Utils::Override
......@@ -41,6 +58,21 @@ module EE
attrs.delete(:import_data_attributes)
end
end
override :delete_project
def delete_project(user_project)
return super unless user_project.adjourned_deletion?
result = destroy_conditionally!(user_project) do
::Projects::MarkForDeletionService.new(user_project, current_user, {}).execute
end
if result[:status] == :success
accepted!
else
render_api_error!(result[:message], 400)
end
end
end
end
end
......
......@@ -27,6 +27,10 @@ module EE
attrs = attrs.except(:default_project_deletion_protection)
end
unless License.feature_available?(:marking_project_for_deletion)
attrs = attrs.except(:deletion_adjourned_period)
end
attrs
end
end
......
......@@ -104,6 +104,13 @@ describe Admin::ApplicationSettingsController do
it_behaves_like 'settings for licensed features'
end
context 'project deletion adjourned period' do
let(:settings) { { deletion_adjourned_period: 6 } }
let(:feature) { :marking_project_for_deletion }
it_behaves_like 'settings for licensed features'
end
context 'additional email footer' do
let(:settings) { { email_additional_text: 'scary legal footer' } }
let(:feature) { :email_additional_text }
......
......@@ -394,4 +394,96 @@ describe ProjectsController do
end
end
end
describe 'DELETE #destroy' do
let(:owner) { create(:user) }
let(:project) { create(:project, namespace: owner.namespace)}
before do
controller.instance_variable_set(:@project, project)
sign_in(owner)
end
context 'feature is available' do
before do
stub_licensed_features(marking_project_for_deletion: true)
end
it 'marks project for deletion' do
delete :destroy, params: { namespace_id: project.namespace, id: project }
expect(project.reload.marked_for_deletion?).to be_truthy
expect(response).to have_gitlab_http_status(302)
expect(response).to redirect_to(project_path(project))
end
it 'does not mark project for deletion because of error' do
message = 'Error'
expect(::Projects::MarkForDeletionService).to receive_message_chain(:new, :execute).and_return({ status: :error, message: message })
delete :destroy, params: { namespace_id: project.namespace, id: project }
expect(response).to have_gitlab_http_status(200)
expect(response).to render_template(:edit)
expect(flash[:alert]).to include(message)
end
context 'when instance setting is set to 0 days' do
it 'deletes project right away' do
allow(Gitlab::CurrentSettings).to receive(:deletion_adjourned_period).and_return(0)
delete :destroy, params: { namespace_id: project.namespace, id: project }
expect(project.marked_for_deletion?).to be_falsey
expect(response).to have_gitlab_http_status(302)
expect(response).to redirect_to(dashboard_projects_path)
end
end
end
context 'feature is not available' do
before do
stub_licensed_features(marking_project_for_deletion: false)
end
it 'deletes project right away' do
delete :destroy, params: { namespace_id: project.namespace, id: project }
expect(project.marked_for_deletion?).to be_falsey
expect(response).to have_gitlab_http_status(302)
expect(response).to redirect_to(dashboard_projects_path)
end
end
end
describe 'POST #restore' do
let(:owner) { create(:user) }
let(:project) { create(:project, namespace: owner.namespace)}
before do
controller.instance_variable_set(:@project, project)
sign_in(owner)
end
it 'restores project deletion' do
post :restore, params: { namespace_id: project.namespace, project_id: project }
expect(project.reload.marked_for_deletion_at).to be_nil
expect(project.reload.archived).to be_falsey
expect(response).to have_gitlab_http_status(302)
expect(response).to redirect_to(edit_project_path(project))
end
it 'does not restore project because of error' do
message = 'Error'
expect(::Projects::RestoreService).to receive_message_chain(:new, :execute).and_return({ status: :error, message: message })
post :restore, params: { namespace_id: project.namespace, project_id: project }
expect(response).to have_gitlab_http_status(200)
expect(response).to render_template(:edit)
expect(flash[:alert]).to include(message)
end
end
end
......@@ -2255,6 +2255,7 @@ describe Project do
context 'when number of days is set to more than 0' do
it 'returns true' do
stub_application_setting(deletion_adjourned_period: 1)
expect(project.adjourned_deletion?).to eq(true)
end
end
......@@ -2262,6 +2263,7 @@ describe Project do
context 'when number of days is set to 0' do
it 'returns false' do
stub_application_setting(deletion_adjourned_period: 0)
expect(project.adjourned_deletion?).to eq(false)
end
end
......
......@@ -180,6 +180,24 @@ describe API::Projects do
end
end
describe 'marked_for_deletion attribute' do
it 'exposed when the feature is available' do
stub_licensed_features(marking_project_for_deletion: true)
get api("/projects/#{project.id}", user)
expect(json_response).to have_key 'marked_for_deletion_at'
end
it 'not exposed when the feature is not available' do
stub_licensed_features(marking_project_for_deletion: false)
get api("/projects/#{project.id}", user)
expect(json_response).not_to have_key 'marked_for_deletion_at'
end
end
describe 'repository_storage attribute' do
context 'when authenticated as an admin' do
let(:admin) { create(:admin) }
......@@ -653,4 +671,92 @@ describe API::Projects do
end
end
end
describe 'POST /projects/:id/restore' do
context 'feature is available' do
before do
stub_licensed_features(marking_project_for_deletion: true)
end
it 'restores project' do
project.update(archived: true, marked_for_deletion_at: 1.day.ago, deleting_user: user)
post api("/projects/#{project.id}/restore", user)
expect(response).to have_gitlab_http_status(201)
expect(json_response['archived']).to be_falsey
expect(json_response['marked_for_deletion_at']).to be_falsey
end
it 'returns error if project is already being deleted' do
message = 'Error'
expect(::Projects::RestoreService).to receive_message_chain(:new, :execute).and_return({ status: :error, message: message })
post api("/projects/#{project.id}/restore", user)
expect(response).to have_gitlab_http_status(400)
expect(json_response["message"]).to eq(message)
end
end
context 'feature is not available' do
before do
stub_licensed_features(marking_project_for_deletion: false)
end
it 'returns error' do
post api("/projects/#{project.id}/restore", user)
expect(response).to have_gitlab_http_status(404)
end
end
end
describe 'DELETE /projects/:id' do
context 'when feature is available' do
before do
stub_licensed_features(marking_project_for_deletion: true)
end
it 'marks project for deletion' do
delete api("/projects/#{project.id}", user)
expect(response).to have_gitlab_http_status(202)
expect(project.reload.marked_for_deletion?).to be_truthy
end
it 'returns error if project cannot be marked for deletion' do
message = 'Error'
expect(::Projects::MarkForDeletionService).to receive_message_chain(:new, :execute).and_return({ status: :error, message: message })
delete api("/projects/#{project.id}", user)
expect(response).to have_gitlab_http_status(400)
expect(json_response["message"]).to eq(message)
end
context 'when instance setting is set to 0 days' do
it 'deletes project right away' do
allow(Gitlab::CurrentSettings).to receive(:deletion_adjourned_period).and_return(0)
delete api("/projects/#{project.id}", user)
expect(response).to have_gitlab_http_status(202)
expect(project.reload.pending_delete).to eq(true)
end
end
end
context 'when feature is not available' do
before do
stub_licensed_features(marking_project_for_deletion: false)
end
it 'deletes project' do
delete api("/projects/#{project.id}", user)
expect(response).to have_gitlab_http_status(202)
expect(project.reload.pending_delete).to eq(true)
end
end
end
end
......@@ -143,6 +143,13 @@ describe API::Settings, 'EE Settings' do
it_behaves_like 'settings for licensed features'
end
context 'deletion adjourned period' do
let(:settings) { { deletion_adjourned_period: 5 } }
let(:feature) { :marking_project_for_deletion }
it_behaves_like 'settings for licensed features'
end
context 'custom file template project' do
let(:settings) { { file_template_project_id: project.id } }
let(:feature) { :custom_file_templates }
......
# frozen_string_literal: true
require 'spec_helper'
describe Projects::MarkForDeletionService do
let(:user) { create(:user) }
let(:marked_for_deletion_at) { nil }
let(:project) do
create(:project,
:repository,
namespace: user.namespace,
marked_for_deletion_at: marked_for_deletion_at)
end
context 'with soft-delete feature turned on' do
before do
stub_licensed_features(marking_project_for_deletion: true)
end
context 'marking project for deletion' do
before do
described_class.new(project, user).execute
end
it 'marks project as archived and marked for deletion' do
expect(Project.unscoped.all).to include(project)
expect(project.archived).to eq(true)
expect(project.marked_for_deletion_at).not_to be_nil
expect(project.deleting_user).to eq(user)
end
end
context 'marking project for deletion once again' do
let(:marked_for_deletion_at) { 2.days.ago }
before do
described_class.new(project, user).execute
end
it 'does not change original date' do
expect(project.marked_for_deletion_at).to eq(marked_for_deletion_at.to_date)
end
end
context 'audit events' do
it 'saves audit event' do
expect { described_class.new(project, user).execute }
.to change { AuditEvent.count }.by(1)
end
end
end
context 'with soft-delete feature turned off' do
context 'marking project for deletion' do
before do
described_class.new(project, user).execute
end
it 'does not change project attributes' do
expect(Project.all).to include(project)
expect(project.archived).to eq(false)
expect(project.marked_for_deletion_at).to be_nil
expect(project.deleting_user).to be_nil
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Projects::RestoreService do
let(:user) { create(:user) }
let(:pending_delete) { nil }
let(:project) do
create(:project,
:repository,
namespace: user.namespace,
marked_for_deletion_at: 1.day.ago,
deleting_user: user,
archived: true,
pending_delete: pending_delete)
end
context 'restoring project' do
before do
described_class.new(project, user).execute
end
it 'marks project as unarchived and not marked for deletion' do
expect(Project.unscoped.all).to include(project)
expect(project.archived).to eq(false)
expect(project.marked_for_deletion_at).to be_nil
expect(project.deleting_user).to eq(nil)
end
end
context 'restoring project already in process of removal' do
let(:deletion_date) { 2.days.ago }
let(:pending_delete) { true }
it 'does not allow to restore' do
expect(described_class.new(project, user).execute).to include(status: :error)
end
end
context 'audit events' do
it 'saves audit event' do
expect { described_class.new(project, user).execute }
.to change { AuditEvent.count }.by(1)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe AdjournedProjectDeletionWorker do
describe "#perform" do
subject(:worker) { described_class.new }
let(:user) { create(:user)}
let(:project) { create(:project, deleting_user: user) }
let(:service) { instance_double(Projects::DestroyService) }
it 'executes destroying project' do
expect(service).to receive(:async_execute)
expect(Projects::DestroyService).to receive(:new).with(project, user).and_return(service)
worker.perform(project.id)
end
it 'stops execution if user was deleted' do
project.update(deleting_user: nil)
expect(Projects::DestroyService).not_to receive(:new)
worker.perform(project.id)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe AdjournedProjectsDeletionCronWorker do
describe "#perform" do
subject(:worker) { described_class.new }
let(:user) { create(:user)}
let(:marked_for_deletion_at) { 14.days.ago }
let!(:project_marked_for_deletion) { create(:project, marked_for_deletion_at: marked_for_deletion_at, deleting_user: user) }
before do
create(:project)
create(:project, marked_for_deletion_at: 3.days.ago)
end
it 'only schedules to delete projects marked for deletion before number of days from settings' do
expect(AdjournedProjectDeletionWorker).to receive(:perform_in).with(0, project_marked_for_deletion.id)
worker.perform
end
context 'marked for deletion exectly before number of days from settings' do
let(:marked_for_deletion_at) { 7.days.ago }
it 'schedules to delete project ' do
expect(AdjournedProjectDeletionWorker).to receive(:perform_in).with(0, project_marked_for_deletion.id)
worker.perform
end
end
context 'when settings are set to not-default number of days' do
before do
create(:project, marked_for_deletion_at: 5.days.ago)
allow(Gitlab::CurrentSettings).to receive(:deletion_adjourned_period).and_return(4)
end
it 'only schedules to delete projects marked for deletion before number of days from settings' do
expect(AdjournedProjectDeletionWorker).to receive(:perform_in).twice
worker.perform
end
end
end
end
......@@ -26,6 +26,14 @@ module API
def verify_update_project_attrs!(project, attrs)
end
def delete_project(user_project)
destroy_conditionally!(user_project) do
::Projects::DestroyService.new(user_project, current_user, {}).async_execute
end
accepted!
end
end
helpers do
......@@ -404,11 +412,7 @@ module API
delete ":id" do
authorize! :remove_project, user_project
destroy_conditionally!(user_project) do
::Projects::DestroyService.new(user_project, current_user, {}).async_execute
end
accepted!
delete_project(user_project)
end
desc 'Mark this project as forked from another'
......
......@@ -2031,13 +2031,16 @@ msgstr ""
msgid "Archive project"
msgstr ""
msgid "Archived project! Repository and other project resources are read only"
msgstr ""
msgid "Archived project! Repository and other project resources are read-only"
msgstr ""
msgid "Archived projects"
msgstr ""
msgid "Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches. <strong>The repository cannot be committed to, and no issues, comments or other entities can be created.</strong>"
msgid "Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end}"
msgstr ""
msgid "Are you setting up GitLab for a company?"
......@@ -5554,6 +5557,9 @@ msgstr ""
msgid "Default classification label"
msgstr ""
msgid "Default deletion adjourned period"
msgstr ""
msgid "Default description template for issues"
msgstr ""
......@@ -5665,6 +5671,9 @@ msgstr ""
msgid "Deleting the license failed. You are not permitted to perform this action."
msgstr ""
msgid "Deletion pending. This project will be removed on %{date}. Repository and other project resources are read-only."
msgstr ""
msgid "Denied authorization of chat nickname %{user_name}."
msgstr ""
......@@ -9339,6 +9348,9 @@ msgstr ""
msgid "How it works"
msgstr ""
msgid "How many days need to pass between marking entity for deletion and actual removing it."
msgstr ""
msgid "How many replicas each Elasticsearch shard has."
msgstr ""
......@@ -12242,6 +12254,9 @@ msgstr ""
msgid "Only Project Members"
msgstr ""
msgid "Only active this projects shows up in the search and on the dashboard."
msgstr ""
msgid "Only admins"
msgstr ""
......@@ -13640,6 +13655,9 @@ msgstr ""
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr ""
msgid "Project '%{project_name}' is restored."
msgstr ""
msgid "Project '%{project_name}' queued for deletion."
msgstr ""
......@@ -13649,6 +13667,9 @@ msgstr ""
msgid "Project '%{project_name}' was successfully updated."
msgstr ""
msgid "Project '%{project_name}' will be deleted on %{date}"
msgstr ""
msgid "Project Badges"
msgstr ""
......@@ -13670,6 +13691,9 @@ msgstr ""
msgid "Project already created"
msgstr ""
msgid "Project already deleted"
msgstr ""
msgid "Project and wiki repositories"
msgstr ""
......@@ -14931,6 +14955,12 @@ msgstr ""
msgid "Removes time estimate."
msgstr ""
msgid "Removing a project places it into a read-only state until %{date}, at which point the project will be permanantly removed. Are you ABSOLUTELY sure?"
msgstr ""
msgid "Removing a project places it into a read-only state until %{date}, at which point the project will be permanently removed."
msgstr ""
msgid "Removing group will cause all child projects and resources to be removed."
msgstr ""
......@@ -15226,6 +15256,12 @@ msgstr ""
msgid "Restart Terminal"
msgstr ""
msgid "Restore project"
msgstr ""
msgid "Restoring the project will prevent the project from being removed on this date and restore people's ability to make changes to it."
msgstr ""
msgid "Restrict access by IP address"
msgstr ""
......@@ -17881,6 +17917,9 @@ msgstr ""
msgid "The remote repository is being updated..."
msgstr ""
msgid "The repository can be commited to, and issues, comments and other entities can be created."
msgstr ""
msgid "The repository for this project does not exist."
msgstr ""
......@@ -18397,6 +18436,9 @@ msgstr ""
msgid "This project path either does not exist or is private."
msgstr ""
msgid "This project will be removed on %{date}"
msgstr ""
msgid "This repository"
msgstr ""
......@@ -19157,7 +19199,7 @@ msgstr ""
msgid "Unarchive project"
msgstr ""
msgid "Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments and other entities can be created. <strong>Once active this project shows up in the search and on the dashboard.</strong>"
msgid "Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end}"
msgstr ""
msgid "Unblock"
......@@ -19268,6 +19310,9 @@ msgstr ""
msgid "Until"
msgstr ""
msgid "Until that time, the project can be restored."
msgstr ""
msgid "Unverified"
msgstr ""
......@@ -20949,6 +20994,9 @@ msgstr ""
msgid "among other things"
msgstr ""
msgid "archived"
msgstr ""
msgid "assign yourself"
msgstr ""
......@@ -21893,6 +21941,9 @@ msgstr ""
msgid "pending comment"
msgstr ""
msgid "pending removal"
msgstr ""
msgid "pipeline"
msgstr ""
......
......@@ -12,6 +12,9 @@ module QA
element :project_path_field
element :change_path_button
element :transfer_button
end
view 'app/views/projects/settings/_archive.html.haml' do
element :archive_project_link
element :unarchive_project_link
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