diff --git a/app/assets/javascripts/vue_shared/components/groups_list/utils.js b/app/assets/javascripts/vue_shared/components/groups_list/utils.js index 5d0a8a005ab49fbe655b3f320cf125fb9189b882..f7121a7c12d916cf45fefab614a08c619da5f3b9 100644 --- a/app/assets/javascripts/vue_shared/components/groups_list/utils.js +++ b/app/assets/javascripts/vue_shared/components/groups_list/utils.js @@ -49,10 +49,7 @@ export const availableGraphQLGroupActions = ({ if (!markedForDeletion) { availableActions.push(ACTION_DELETE); // Groups with self deletion scheduled can be deleted immediately - } else if ( - isSelfDeletionScheduled && - (userPermissions.adminAllResources || !gon?.features?.disallowImmediateDeletion) - ) { + } else if (isSelfDeletionScheduled && gon?.allow_immediate_namespaces_deletion) { availableActions.push(ACTION_DELETE_IMMEDIATELY); } } diff --git a/app/assets/javascripts/vue_shared/components/projects_list/project_list_item_actions.vue b/app/assets/javascripts/vue_shared/components/projects_list/project_list_item_actions.vue index 2cf929272c0b3e675da3b9951ff64869f8898dd4..2454f66d2ddb08a4304ad8473e42b46e9a7c5d82 100644 --- a/app/assets/javascripts/vue_shared/components/projects_list/project_list_item_actions.vue +++ b/app/assets/javascripts/vue_shared/components/projects_list/project_list_item_actions.vue @@ -7,6 +7,7 @@ import ListActions from '~/vue_shared/components/list_actions/list_actions.vue'; import { ACTION_ARCHIVE, ACTION_DELETE, + ACTION_DELETE_IMMEDIATELY, ACTION_EDIT, ACTION_RESTORE, ACTION_UNARCHIVE, @@ -76,6 +77,9 @@ export default { [ACTION_DELETE]: { action: this.onActionDelete, }, + [ACTION_DELETE_IMMEDIATELY]: { + action: this.onActionDelete, + }, }; }, }, diff --git a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue index 9c1da5b181d1dfd2a748530ca84e12a1f688361c..1ae025e7eda09f3f95a6a7eebbaaea44f4a65e5c 100644 --- a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue +++ b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue @@ -12,7 +12,10 @@ import { ACCESS_LEVEL_LABELS, ACCESS_LEVEL_NO_ACCESS_INTEGER } from '~/access_le import { FEATURABLE_ENABLED } from '~/featurable/constants'; import { __, s__, n__, sprintf } from '~/locale'; import { numberToHumanSize, numberToMetricPrefix } from '~/lib/utils/number_utils'; -import { ACTION_DELETE } from '~/vue_shared/components/list_actions/constants'; +import { + ACTION_DELETE, + ACTION_DELETE_IMMEDIATELY, +} from '~/vue_shared/components/list_actions/constants'; import DeleteModal from '~/projects/components/shared/delete_modal.vue'; import ProjectListItemDelayedDeletionModalFooter from '~/vue_shared/components/projects_list/project_list_item_delayed_deletion_modal_footer.vue'; import { @@ -181,7 +184,10 @@ export default { return this.project.availableActions?.length; }, hasActionDelete() { - return this.project.availableActions?.includes(ACTION_DELETE); + return ( + this.project.availableActions?.includes(ACTION_DELETE) || + this.project.availableActions?.includes(ACTION_DELETE_IMMEDIATELY) + ); }, pipelineStatus() { return this.project.pipeline?.detailedStatus; diff --git a/app/assets/javascripts/vue_shared/components/projects_list/utils.js b/app/assets/javascripts/vue_shared/components/projects_list/utils.js index 506a359d5f57061d70d1dd9e4df8d46fe1c4624a..71988c2351da6113860530948597d9823c654024 100644 --- a/app/assets/javascripts/vue_shared/components/projects_list/utils.js +++ b/app/assets/javascripts/vue_shared/components/projects_list/utils.js @@ -1,6 +1,7 @@ import { ACTION_EDIT, ACTION_DELETE, + ACTION_DELETE_IMMEDIATELY, ACTION_RESTORE, ACTION_UNARCHIVE, ACTION_ARCHIVE, @@ -39,11 +40,8 @@ export const availableGraphQLProjectActions = ({ if (!markedForDeletion) { availableActions.push(ACTION_DELETE); // Projects with self deletion scheduled can be deleted immediately - } else if ( - isSelfDeletionScheduled && - (userPermissions.adminAllResources || !gon?.features?.disallowImmediateDeletion) - ) { - availableActions.push(ACTION_DELETE); + } else if (isSelfDeletionScheduled && gon?.allow_immediate_namespaces_deletion) { + availableActions.push(ACTION_DELETE_IMMEDIATELY); } } diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 4cdb93d6158e7e08cb83ba33ccff8a6f46cec257..86c6e443678dbeca17d17fe55ec402af446ed4c5 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -176,11 +176,9 @@ def destroy ::Gitlab::Utils.to_boolean(params.permit(:permanently_remove)[:permanently_remove]) # Admin frontend uses this endpoint to force-delete groups - if Feature.enabled?(:disallow_immediate_deletion, current_user) && !current_user.can_admin_all_resources? - return access_denied! - end + return destroy_immediately if Gitlab::CurrentSettings.allow_immediate_namespaces_deletion_for_user?(current_user) - return destroy_immediately + return access_denied! end result = ::Groups::MarkForDeletionService.new(group, current_user).execute diff --git a/app/controllers/organizations/groups_controller.rb b/app/controllers/organizations/groups_controller.rb index 701a3e9644db503e2adb4074d14e3a784419216e..d05ccbdc00acf0b0c70a0c02eaaf74c2696e9120 100644 --- a/app/controllers/organizations/groups_controller.rb +++ b/app/controllers/organizations/groups_controller.rb @@ -31,9 +31,11 @@ def destroy if group.self_deletion_scheduled? && ::Gitlab::Utils.to_boolean(params.permit(:permanently_remove)[:permanently_remove]) - return access_denied! if Feature.enabled?(:disallow_immediate_deletion, current_user) + if Gitlab::CurrentSettings.allow_immediate_namespaces_deletion_for_user?(current_user) + return destroy_immediately + end - return destroy_immediately + return access_denied! end result = ::Groups::MarkForDeletionService.new(group, current_user).execute diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 15778d7f793c53efd00155884f64617fb4d37dc8..d945fd380cc27aba2d58b1f7d3ace8683518153b 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -195,9 +195,9 @@ def destroy if @project.self_deletion_scheduled? && ::Gitlab::Utils.to_boolean(params.permit(:permanently_delete)[:permanently_delete]) - return access_denied! if Feature.enabled?(:disallow_immediate_deletion, current_user) + return destroy_immediately if Gitlab::CurrentSettings.allow_immediate_namespaces_deletion_for_user?(current_user) - return destroy_immediately + return access_denied! end result = ::Projects::MarkForDeletionService.new(@project, current_user).execute diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 73d3b4df7c8328add9e7b4f1387e57e4c33ed082..f03609554ba8110b5b4eebb1ef12e9ed753cea8d 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -261,6 +261,7 @@ def visible_attributes :after_sign_up_text, :akismet_api_key, :akismet_enabled, + :allow_immediate_namespaces_deletion, :allow_local_requests_from_hooks_and_services, :allow_local_requests_from_web_hooks_and_services, :allow_local_requests_from_system_hooks, diff --git a/app/helpers/namespaces/deletable_helper.rb b/app/helpers/namespaces/deletable_helper.rb index 9530fe95e9508a13b96a49307aa2829ca255ed01..a62cccf77b01e3b299ebd25ee44cb280aea3eced 100644 --- a/app/helpers/namespaces/deletable_helper.rb +++ b/app/helpers/namespaces/deletable_helper.rb @@ -115,6 +115,15 @@ def delete_immediately_namespace_scheduled_for_deletion_message(namespace) ) end + def delete_immediately_unavailable_due_to_ancestor_message(namespace) + messages = { + group: _('To delete immediately this group, you must delete immediately its parent group.'), + project: _('To delete immediately this project, you must delete immediately its parent group.') + } + + message_for_namespace(namespace, messages) + end + def group_confirm_modal_data( group:, remove_form_id: nil, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 2a73de8dd290c7e0e3edb0578222f05f9b983477..b0a64e0eed28449a1854a7357df9993e1b2d0b1e 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -973,6 +973,16 @@ def self.kroki_formats_attributes allow_nil: false, inclusion: { in: [true, false], message: N_('must be a boolean value') } + validates :allow_immediate_namespaces_deletion, + allow_nil: false, + inclusion: { in: [true, false], message: N_('must be a boolean value') }, + unless: :gitlab_dedicated_instance + + validates :allow_immediate_namespaces_deletion, + allow_nil: false, + inclusion: { in: [false], message: N_('cannot be enabled on Dedicated') }, + if: :gitlab_dedicated_instance + validates :allow_runner_registration_token, allow_nil: false, inclusion: { in: [true, false], message: N_('must be a boolean value') } diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 617dd771dd72bb75390e255cf8134ef63a32df9f..69989f2218ee00913822c2dee07ddf5332952982 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -40,6 +40,7 @@ def defaults # rubocop:disable Metrics/AbcSize after_sign_up_text: nil, akismet_enabled: false, akismet_api_key: nil, + allow_immediate_namespaces_deletion: true, allow_local_requests_from_system_hooks: true, allow_local_requests_from_web_hooks_and_services: false, allow_possible_spam: false, @@ -675,6 +676,13 @@ def repository_storages_with_default_weight Hash[storages_map] end + def allow_immediate_namespaces_deletion_for_user?(user) + # Keep the previous behavior when the feature flag is disabled + return true unless Feature.enabled?(:allow_immediate_namespaces_deletion, user) + + allow_immediate_namespaces_deletion? || user&.can_admin_all_resources? + end + private def set_max_key_restriction!(key_type) diff --git a/app/views/admin/application_settings/_deletion_protection_settings.html.haml b/app/views/admin/application_settings/_deletion_protection_settings.html.haml index ba7757b648035045e8d9d6c349b09a7da5f9d1d6..ec41679f0b7627ae592433626f66c63c064123e8 100644 --- a/app/views/admin/application_settings/_deletion_protection_settings.html.haml +++ b/app/views/admin/application_settings/_deletion_protection_settings.html.haml @@ -1 +1,13 @@ #js-admin-deletion-protection-settings{ data: deletion_protection_data } + +- if Feature.enabled?(:allow_immediate_namespaces_deletion, current_user) + - form = local_assigns.fetch(:form) + - help_page_link = link_to('', help_page_path('administration/settings/visibility_and_access_controls.md', anchor: 'immediate-deletion'), target: '_blank', rel: 'noopener noreferrer') + + %fieldset.form-group.gl-form-group + %legend.col-form-label + %small.form-text.gl-text-subtle + = s_('ImmediateNamespacesDeletionSettings|Prevent non-administrators from immediately deleting groups and projects.') + = form.gitlab_ui_checkbox_component :allow_immediate_namespaces_deletion, s_('ImmediateNamespacesDeletionSettings|Allow immediate deletion') do |c| + - c.with_help_text do + = safe_format(s_('ImmediateNamespacesDeletionSettings|Groups and projects marked for deletion can be immediately deleted with a second deletion action, bypassing the configured retention period. %{link_start}Learn more.%{link_end}'), tag_pair(help_page_link, :link_start, :link_end)) diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml index 9092267563a9fc0605b4320c9434a75b453b3b31..ff7c7afc57e40a7437831c254f136a18b002ba16 100644 --- a/app/views/admin/application_settings/_visibility_and_access.html.haml +++ b/app/views/admin/application_settings/_visibility_and_access.html.haml @@ -4,7 +4,7 @@ %fieldset = render 'shared/project_creation_levels', f: f, method: :default_project_creation, legend: s_('ProjectCreationLevel|Default minimum role required to create projects') = render_if_exists 'admin/application_settings/default_project_deletion_protection_setting', form: f - = render_if_exists 'admin/application_settings/deletion_protection_settings', form: f + = render 'admin/application_settings/deletion_protection_settings', 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) diff --git a/app/views/groups/settings/_immediately_remove.html.haml b/app/views/groups/settings/_immediately_remove.html.haml index 91ef408aff1bce2208125e078fd21bc2756f253f..09da3889d436fe2844f1c1cd56fa7efaf935e518 100644 --- a/app/views/groups/settings/_immediately_remove.html.haml +++ b/app/views/groups/settings/_immediately_remove.html.haml @@ -1,17 +1,37 @@ -- return if Feature.enabled?(:disallow_immediate_deletion, current_user) -- return unless group.self_deletion_scheduled? +- return unless can?(current_user, :remove_group, group) +- return unless group.scheduled_for_deletion_in_hierarchy_chain? - remove_form_id = local_assigns.fetch(:remove_form_id, nil) +- allow_immediate_namespaces_deletion = Gitlab::CurrentSettings.allow_immediate_namespaces_deletion_for_user?(current_user) +- immediate_deletion_disabled = group.self_deletion_scheduled? && allow_immediate_namespaces_deletion +- deletion_available = !group.scheduled_for_deletion_in_hierarchy_chain? || immediate_deletion_disabled +- component_class = deletion_available ? 'gl-bg-feedback-danger' : '' +- h4_class = deletion_available ? 'gl-text-feedback-danger' : '' +- title = group.scheduled_for_deletion_in_hierarchy_chain? ? s_('GroupSettings|Delete group immediately') : s_('GroupSettings|Delete group') -= render Pajamas::CardComponent.new(body_options: { class: 'gl-bg-feedback-danger' }) do |c| += render Pajamas::CardComponent.new(body_options: { class: component_class }) do |c| - c.with_header do .gl-flex.gl-grow - %h4.gl-text-base.gl-leading-24.gl-m-0.gl-text-feedback-danger= _('Delete group immediately') + %h4.gl-text-base.gl-leading-24.gl-m-0{ class: h4_class } + = title + - unless deletion_available + - tooltip = allow_immediate_namespaces_deletion && group.ancestor_scheduled_for_deletion? ? delete_immediately_unavailable_due_to_ancestor_message(group) : s_('GroupSettings|Immediate deletion is disabled') + %span.has-tooltip{ title: tooltip } + = sprite_icon('cancel', css_class: "gl-icon gl-cursor-pointer", size: 16) - c.with_body do - = form_tag(group, method: :delete, id: remove_form_id) do - %p= delete_immediately_namespace_scheduled_for_deletion_message(group) + - if group.self_deletion_scheduled? && allow_immediate_namespaces_deletion + -# Immediate deletion is available + = form_tag(group, method: :delete, id: remove_form_id) do + %p= delete_immediately_namespace_scheduled_for_deletion_message(group) + = hidden_field_tag(:permanently_remove, true) - = hidden_field_tag(:permanently_remove, true) + = render 'groups/settings/remove_button', group: group, remove_form_id: remove_form_id, button_text: title - = render 'groups/settings/remove_button', group: group, remove_form_id: remove_form_id, button_text: _('Delete group immediately') + - elsif group.scheduled_for_deletion_in_hierarchy_chain? + -# Immediate deletion not available: either because of ancestor, or instance setting + - help_page_link = link_to('', help_page_path('user/group/_index.md', anchor: 'delete-a-group-immediately'), target: '_blank', rel: 'noopener noreferrer') + %div + = self_or_ancestors_deletion_in_progress_or_scheduled_message(group) + - if group.self_deletion_scheduled? + = safe_format(s_('GroupSettings|%{link_start}How do I immediately delete a group?%{link_end}'), tag_pair(help_page_link, :link_start, :link_end)) diff --git a/app/views/projects/_delete.html.haml b/app/views/projects/_delete.html.haml index a5765a77493fa6112f62644dd41f47111d632190..26a3979e1bccda3cf3336d1550f5010cf055122f 100644 --- a/app/views/projects/_delete.html.haml +++ b/app/views/projects/_delete.html.haml @@ -1,17 +1,36 @@ - return unless can?(current_user, :remove_project, @project) -- return if Feature.enabled?(:disallow_immediate_deletion, current_user) && @project.self_deletion_scheduled? -- return if @project.ancestor_scheduled_for_deletion? -- title = @project.self_deletion_scheduled? ? _('Delete project immediately') : _('Delete project') +- allow_immediate_namespaces_deletion = Gitlab::CurrentSettings.allow_immediate_namespaces_deletion_for_user?(current_user) +- immediate_deletion_disabled = @project.self_deletion_scheduled? && allow_immediate_namespaces_deletion +- deletion_available = !@project.scheduled_for_deletion_in_hierarchy_chain? || immediate_deletion_disabled +- component_class = deletion_available ? 'gl-bg-feedback-danger' : '' +- h4_class = deletion_available ? 'gl-text-feedback-danger' : '' +- title = @project.scheduled_for_deletion_in_hierarchy_chain? ? s_('ProjectSettings|Delete project immediately') : s_('ProjectSettings|Delete project') -= render Pajamas::CardComponent.new(body_options: { class: 'gl-bg-feedback-danger' }) do |c| += render Pajamas::CardComponent.new(body_options: { class: component_class }) do |c| - c.with_header do .gl-flex.gl-grow - %h4.gl-text-base.gl-leading-24.gl-m-0.gl-text-feedback-danger= title + %h4.gl-text-base.gl-leading-24.gl-m-0{ class: h4_class } + = title + - unless deletion_available + - tooltip = allow_immediate_namespaces_deletion && @project.ancestor_scheduled_for_deletion? ? delete_immediately_unavailable_due_to_ancestor_message(@project) : s_('ProjectSettings|Immediate deletion is disabled') + %span.has-tooltip{ title: tooltip } + = sprite_icon('cancel', css_class: "gl-icon gl-cursor-pointer", size: 16) - c.with_body do - - if @project.self_deletion_scheduled? + - if @project.self_deletion_scheduled? && allow_immediate_namespaces_deletion + -# Immediate deletion is available %p= delete_immediately_namespace_scheduled_for_deletion_message(@project) - #js-project-delete-button{ data: project_delete_immediately_button_data(@project, _('Delete project immediately')) } + #js-project-delete-button{ data: project_delete_immediately_button_data(@project, title) } + + - elsif @project.scheduled_for_deletion_in_hierarchy_chain? + -# Immediate deletion not available: either because of ancestor, or instance setting + - help_page_link = link_to('', help_page_path('user/project/working_with_projects.md', anchor: 'delete-a-project-immediately'), target: '_blank', rel: 'noopener noreferrer') + %p + = self_or_ancestors_deletion_in_progress_or_scheduled_message(@project) + - if @project.self_deletion_scheduled? + = safe_format(s_('ProjectSettings|%{link_start}How do I immediately delete a project?%{link_end}'), tag_pair(help_page_link, :link_start, :link_end)) + - else + -# Neither the project or its ancestor is scheduled for deletion = render 'delete_delayed' diff --git a/config/application_setting_columns/allow_immediate_namespaces_deletion.yml b/config/application_setting_columns/allow_immediate_namespaces_deletion.yml new file mode 100644 index 0000000000000000000000000000000000000000..4157a6400a3cba68b16dc431f9c4073a2e2d136a --- /dev/null +++ b/config/application_setting_columns/allow_immediate_namespaces_deletion.yml @@ -0,0 +1,14 @@ +--- +api_type: boolean +attr: allow_immediate_namespaces_deletion +clusterwide: true +column: allow_immediate_namespaces_deletion +db_type: boolean +default: 'true' +description: Groups and projects marked for deletion can be immediately deleted with + a second deletion action, bypassing the configured retention period. Defaults to + `true`. +encrypted: false +gitlab_com_different_than_default: true +jihu: false +not_null: true diff --git a/config/application_setting_columns/outbound_local_requests_whitelist.yml b/config/application_setting_columns/outbound_local_requests_whitelist.yml index 7558fe26f1eb30cbedab3c68c7b1cd7eacdf8fd6..691715c3c7b9bc9347e2cb25186a5b953d7d703d 100644 --- a/config/application_setting_columns/outbound_local_requests_whitelist.yml +++ b/config/application_setting_columns/outbound_local_requests_whitelist.yml @@ -6,7 +6,8 @@ column: outbound_local_requests_whitelist db_type: character default: "'{}'::character" description: Define a list of trusted domains or IP addresses to which local requests - are allowed when local requests for webhooks and integrations are disabled. + are allowed when local requests for webhooks and integrations are disabled. Currently, + this attribute can not be updated. For details, see [issue 569729](https://gitlab.com/gitlab-org/gitlab/-/issues/569729). encrypted: false gitlab_com_different_than_default: true jihu: false diff --git a/config/feature_flags/gitlab_com_derisk/disallow_immediate_deletion.yml b/config/feature_flags/gitlab_com_derisk/disallow_immediate_deletion.yml deleted file mode 100644 index 9a11c78ca5c13621acfc9d50bf96e09113cffdf9..0000000000000000000000000000000000000000 --- a/config/feature_flags/gitlab_com_derisk/disallow_immediate_deletion.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: disallow_immediate_deletion -description: -feature_issue_url: -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/201957 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/562308 -milestone: '18.4' -group: group::organizations -type: gitlab_com_derisk -default_enabled: false diff --git a/config/feature_flags/wip/allow_immediate_namespaces_deletion.yml b/config/feature_flags/wip/allow_immediate_namespaces_deletion.yml new file mode 100644 index 0000000000000000000000000000000000000000..fd2b394dae4ef04e26d1efeec497c6cfc2e4ebc3 --- /dev/null +++ b/config/feature_flags/wip/allow_immediate_namespaces_deletion.yml @@ -0,0 +1,10 @@ +--- +name: allow_immediate_namespaces_deletion +description: +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/569453 +introduced_by_url: +rollout_issue_url: +milestone: '18.4' +group: group::organizations +type: wip +default_enabled: false diff --git a/db/fixtures/production/010_settings.rb b/db/fixtures/production/010_settings.rb index 5193a02e39acfc3834320de1ea6aa93e67f2cc62..b308c1dd76d1760b7df7313683b809b84349c2d3 100644 --- a/db/fixtures/production/010_settings.rb +++ b/db/fixtures/production/010_settings.rb @@ -33,3 +33,9 @@ def save(settings, topic) settings = Gitlab::CurrentSettings.current_application_settings settings.ci_job_token_signing_key = OpenSSL::PKey::RSA.new(2048).to_pem save(settings, 'CI Job Token signing key') + +settings = Gitlab::CurrentSettings.current_application_settings +if settings.gitlab_dedicated_instance? + settings.allow_immediate_namespaces_deletion = false + save(settings, 'Disable immediate namespace deletion') +end diff --git a/db/migrate/20250910143855_add_allow_immediate_namespaces_deletion_to_application_settings.rb b/db/migrate/20250910143855_add_allow_immediate_namespaces_deletion_to_application_settings.rb new file mode 100644 index 0000000000000000000000000000000000000000..5b4aa260143492f4f17218f3a69e51d0c318face --- /dev/null +++ b/db/migrate/20250910143855_add_allow_immediate_namespaces_deletion_to_application_settings.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class AddAllowImmediateNamespacesDeletionToApplicationSettings < Gitlab::Database::Migration[2.3] + milestone '18.5' + + def change + add_column :application_settings, + :allow_immediate_namespaces_deletion, + :boolean, + default: true, + null: false, + if_not_exists: true + end +end diff --git a/db/post_migrate/20250910144106_set_allow_immediate_namespaces_deletion_to_false_on_saas.rb b/db/post_migrate/20250910144106_set_allow_immediate_namespaces_deletion_to_false_on_saas.rb new file mode 100644 index 0000000000000000000000000000000000000000..616141b20dab48ac4cb557c4faf9bcd8752c878e --- /dev/null +++ b/db/post_migrate/20250910144106_set_allow_immediate_namespaces_deletion_to_false_on_saas.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class SetAllowImmediateNamespacesDeletionToFalseOnSaas < Gitlab::Database::Migration[2.3] + milestone '18.5' + restrict_gitlab_migration gitlab_schema: :gitlab_main + + class MigrationApplicationSettings < MigrationRecord + self.table_name = 'application_settings' + end + + def up + return unless should_run?(MigrationApplicationSettings.last) + + # On GitLab.com and Dedicated we don't allow bypassing deletion retention period + MigrationApplicationSettings.update_all(allow_immediate_namespaces_deletion: false) + end + + def down + return unless should_run?(MigrationApplicationSettings.last) + + # Revert back to the default value + MigrationApplicationSettings.update_all(allow_immediate_namespaces_deletion: true) + end + + private + + def should_run?(application_setting) + Gitlab.com? || application_setting&.gitlab_dedicated_instance + end +end diff --git a/db/schema_migrations/20250910143855 b/db/schema_migrations/20250910143855 new file mode 100644 index 0000000000000000000000000000000000000000..009cd41b7a83b6949b7776df581efcbd35fb1551 --- /dev/null +++ b/db/schema_migrations/20250910143855 @@ -0,0 +1 @@ +03e4102317a78192973f3738c77f9c4bc4f5230cc801ecacc7dc24d6efc941a9 \ No newline at end of file diff --git a/db/schema_migrations/20250910144106 b/db/schema_migrations/20250910144106 new file mode 100644 index 0000000000000000000000000000000000000000..83a649c2c99ca66c49045c159f4f00a7b02895ac --- /dev/null +++ b/db/schema_migrations/20250910144106 @@ -0,0 +1 @@ +f5113244f5c91e4babef5e6f13ecf63d63da971d35411dd39de6e8f132924fc5 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index f90d727788a50a3c48a68f9f8601ceade7133f90..dffabf1f706a263b01f8c5cda2786a2c35dbc26f 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -10757,6 +10757,7 @@ CREATE TABLE application_settings ( usage_ping_generation_enabled boolean DEFAULT true NOT NULL, duo_remote_flows_enabled boolean DEFAULT true NOT NULL, lock_duo_remote_flows_enabled boolean DEFAULT false NOT NULL, + allow_immediate_namespaces_deletion boolean DEFAULT true NOT NULL, CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)), CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)), CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)), diff --git a/doc/administration/settings/visibility_and_access_controls.md b/doc/administration/settings/visibility_and_access_controls.md index f3cf4090bf191dbbb6d9a6f5101bef142ceac96d..7607d979c0bcb176aeccf1a2f232d85808022a0a 100644 --- a/doc/administration/settings/visibility_and_access_controls.md +++ b/doc/administration/settings/visibility_and_access_controls.md @@ -99,17 +99,10 @@ To disable the restriction: - Enabled delayed deletion for projects by default [on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/393622) and [on GitLab Self-Managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119606) in GitLab 16.0. - [Changed to default behavior for groups](https://gitlab.com/gitlab-org/gitlab/-/issues/389557) on the Premium and Ultimate tier in GitLab 16.0. - [Moved](https://gitlab.com/groups/gitlab-org/-/epics/17208) from GitLab Premium to GitLab Free in 18.0. -- Support for disallowing immediate deletion for groups or projects scheduled for deletion [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/201957) in GitLab 18.4 [with a flag](../../administration/feature_flags/_index.md) named `disallow_immediate_deletion`. Disabled by default. +- [Instance setting](#immediate-deletion) to allow immediate deletion for groups or projects scheduled for deletion [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/204669) in GitLab 18.5. Enabled by default. Disabled on GitLab.com and Dedicated. {{< /history >}} -{{< alert type="flag" >}} - -The availability of this feature is controlled by a feature flag. -For more information, see the history. - -{{< /alert >}} - These protections help guard against accidental deletion of groups and projects on your instance. ### Retention period @@ -129,6 +122,41 @@ To configure deletion protection for groups and projects: 1. Scroll to **Deletion protection** and set the retention period to a value between `1` and `90` days. 1. Select **Save changes**. +### Immediate deletion + +{{< history >}} + +- Instance setting to allow immediate deletion for groups or projects scheduled for deletion + [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/204669) in GitLab 18.5 + [with a flag](../../administration/feature_flags/_index.md) named `allow_immediate_namespaces_deletion`. + Enabled by default on self-managed, but disabled on GitLab.com and Dedicated. + +{{< /history >}} + +{{< alert type="flag" >}} + +The availability of this feature is controlled by a feature flag. +For more information, see the history. + +{{< /alert >}} + +By default, immediate deletion is allowed for groups and projects marked for deletion. This allows users +to effectively bypass the configured retention period and delete groups or projects immediately. + +This can be disabled, so that groups and projects are only deleted automatically after the configured retention period: + +1. On the left sidebar, at the bottom, select **Admin**. +1. Select **Settings** > **General**. +1. Expand **Visibility and access controls**. +1. Scroll to **Immediate deletion** and uncheck the checkbox. +1. Select **Save changes**. + +{{< alert type="note" >}} + +Administrators can always immediately delete groups and projects through the Admin pages. + +{{< /alert >}} + ### Override defaults and delete immediately To override the delay, and immediately delete a project marked for removal: diff --git a/doc/api/groups.md b/doc/api/groups.md index bac4353cbe78fdcf5ac3daa75824967c3ed23736..561b508403bc05960ec100400b0c5b624c33d5b1 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -1372,7 +1372,10 @@ curl --request POST --header "PRIVATE-TOKEN: " \ {{< history >}} - Marking groups for deletion [available](https://gitlab.com/groups/gitlab-org/-/epics/17208) on Free tier in GitLab 18.0. -- `permanently_remove` was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/201957) in GitLab 18.4 [with a flag](../administration/feature_flags/_index.md) named `disallow_immediate_deletion`. +- Since GitLab 18.5, `permanently_remove` is [not permitted](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/204669) + when the immediate deletion + [instance setting](../administration/settings/visibility_and_access_controls.md#immediate-deletion) + is disabled. The setting is enabled by default on self-managed, but disabled on GitLab.com and Dedicated. {{< /history >}} @@ -1388,14 +1391,23 @@ Marks a group for deletion. Groups are deleted at the end of the retention perio This endpoint can also immediately delete a subgroup that was previously marked for deletion. +{{< alert type="warning" >}} + +On GitLab.com, after a group is deleted, its data is retained for 30 days, and immediate deletion is not available. +If you really need to delete a group immediately on GitLab.com, you can open a [support ticket](https://about.gitlab.com/support/). + +{{< /alert >}} + ```plaintext DELETE /groups/:id ``` Parameters: +| Attribute | Type | Required | Description | +|-----------|----------------|----------|-------------| | `id` | integer or string | yes | The ID or [URL-encoded path](rest/_index.md#namespaced-paths) of the group. | -| `permanently_remove` | boolean/string | no | [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/201957) in GitLab 18.4. If `true`, immediately deletes a subgroup that is already marked for deletion. Cannot delete top-level groups. | +| `permanently_remove` | boolean/string | no | If `true`, immediately deletes a subgroup that is already marked for deletion. Cannot delete top-level groups. Disabled on GitLab.com and Dedicated. | | `full_path` | string | no | The full path to the subgroup. Used to confirm deletion of the subgroup. If `permanently_remove` is `true`, this attribute is required. To find the subgroup path, see the [group details](groups.md#get-a-single-group). | The response is `202 Accepted` if the user has authorization. diff --git a/doc/api/projects.md b/doc/api/projects.md index 236d6abfb280bd0a018a2a98f10c224a27414092..e6bd5ccc7292b1f42ad99fc7d8b384c7b2905be3 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -2167,21 +2167,29 @@ Example response: - Immediately deleting projects was [enabled on GitLab.com and GitLab Self-Managed](https://gitlab.com/gitlab-org/gitlab/-/issues/396500) in GitLab 15.11. - [Marking project for deletion was moved](https://gitlab.com/groups/gitlab-org/-/epics/17208) from GitLab Premium to GitLab Free in 18.0. -- `permanently_remove` was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/201957) in GitLab 18.4 [with a flag](../administration/feature_flags/_index.md) named `disallow_immediate_deletion`. +- Since GitLab 18.5, `permanently_remove` is [not permitted](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/204669) + when the immediate deletion + [instance setting](../administration/settings/visibility_and_access_controls.md#immediate-deletion) + is disabled. The setting is enabled by default on self-managed, but disabled on GitLab.com and Dedicated. {{< /history >}} -Delete a project. This endpoint: +Prerequisites: + +- You must be an administrator or have the Owner role for the project. + +Marks a project for deletion. Projects are deleted at the end of the retention period: + +- On GitLab.com, projects are retained for 30 days. +- On GitLab Self-Managed, the retention period is controlled by the + [instance settings](../administration/settings/visibility_and_access_controls.md#deletion-protection). -- Deletes a project including all associated resources, including issues and merge requests. -- Marks the project for deletion. On GitLab.com, by default, the deletion happens 30 days later. On GitLab Self-Managed, - the retention period depends on the [instance settings](../administration/settings/visibility_and_access_controls.md#deletion-protection). -- Deletes project immediately if the project is marked for deletion (GitLab 15.11 and later). +This endpoint can also immediately delete a project that was previously marked for deletion. {{< alert type="warning" >}} -The option to delete projects immediately from deletion protection settings in the **Admin** area was -[deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/389557) in GitLab 15.9 and removed in GitLab 16.0. +On GitLab.com, after a project is deleted, its data is retained for 30 days, and immediate deletion is not available. +If you really need to delete a project immediately on GitLab.com, you can open a [support ticket](https://about.gitlab.com/support/). {{< /alert >}} @@ -2195,7 +2203,7 @@ Supported attributes: |:---------------------|:------------------|:---------|:------------| | `id` | integer or string | Yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths). | | `full_path` | string | no | Full path of project to use with `permanently_remove`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/396500) in GitLab 15.11 for Premium and Ultimate only and moved to GitLab Free in 18.0. To find the project path, use `path_with_namespace` from [get single project](projects.md#get-a-single-project). | -| `permanently_remove` | boolean/string | no | [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/201957) in GitLab 18.4. Immediately deletes a project if it is marked for deletion. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/396500) in GitLab 15.11 for Premium and Ultimate only and moved to GitLab Free in 18.0. | +| `permanently_remove` | boolean/string | no | Immediately deletes a project if it is marked for deletion. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/396500) in GitLab 15.11 for Premium and Ultimate only and moved to GitLab Free in 18.0. Disabled on GitLab.com and Dedicated. | ### Restore a project marked for deletion diff --git a/doc/api/settings.md b/doc/api/settings.md index 88c497290052a2f7fc0ab4776d91f2fbecabcf5a..51a0c04530a12fc1949d3165f4da838fcee2d8ae 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -234,7 +234,7 @@ these parameters: {{< /history >}} -Updates the current [application settings](#available-settings) for this GitLab instance. +Updates the current [application settings](#available-settings) for this GitLab instance. ```plaintext PUT /application/settings @@ -456,6 +456,7 @@ to configure other related settings. These requirements are | `allow_all_integrations` | boolean | no | When `false`, only integrations in `allowed_integrations` are allowed on the instance. Ultimate only. | | `allowed_integrations` | array of strings | no | When `allow_all_integrations` is `false`, only integrations in this list are allowed on the instance. Ultimate only. | | `allow_account_deletion` | boolean | no | Set to `true` to allow users to delete their accounts. Premium and Ultimate only. | +| `allow_immediate_namespaces_deletion` | boolean | no | Groups and projects marked for deletion can be immediately deleted with a second deletion action, bypassing the configured retention period. Defaults to `true`. | | `allow_group_owners_to_manage_ldap` | boolean | no | Set to `true` to allow group owners to manage LDAP. Premium and Ultimate only. | | `allow_local_requests_from_hooks_and_services` | boolean | no | (Deprecated: Use `allow_local_requests_from_web_hooks_and_services` instead) Allow requests to the local network from webhooks and integrations. | | `allow_local_requests_from_system_hooks` | boolean | no | Allow requests to the local network from system hooks. | diff --git a/doc/development/cells/application_settings_analysis.md b/doc/development/cells/application_settings_analysis.md index f72919501643dbf1dea75825728317b53c4dbfa7..baa50b12c8409a785fafe1b5983937ade0fab0f9 100644 --- a/doc/development/cells/application_settings_analysis.md +++ b/doc/development/cells/application_settings_analysis.md @@ -14,12 +14,12 @@ title: Application Settings analysis ## Statistics -- Number of attributes: 504 +- Number of attributes: 505 - Number of encrypted attributes: 42 (8.0%) -- Number of attributes documented: 295 (59.0%) -- Number of attributes on GitLab.com different from the defaults: 223 (44.0%) -- Number of attributes with `clusterwide` set: 504 (100.0%) -- Number of attributes with `clusterwide: true` set: 131 (26.0%) +- Number of attributes documented: 296 (59.0%) +- Number of attributes on GitLab.com different from the defaults: 224 (44.0%) +- Number of attributes with `clusterwide` set: 505 (100.0%) +- Number of attributes with `clusterwide: true` set: 132 (26.0%) ## Individual columns @@ -34,6 +34,7 @@ title: Application Settings analysis | `akismet_enabled` | `false` | `boolean` | `boolean` | `false` | `false` | `false` | `false`| `true` | | `allow_account_deletion` | `false` | `boolean` | `boolean` | `true` | `true` | `false` | `false`| `true` | | `allow_deploy_tokens_and_keys_with_external_authn` | `false` | `boolean` | `` | `true` | `false` | `false` | `false`| `false` | +| `allow_immediate_namespaces_deletion` | `false` | `boolean` | `boolean` | `true` | `true` | `true` | `true`| `true` | | `allow_group_owners_to_manage_ldap` | `false` | `boolean` | `boolean` | `true` | `true` | `false` | `false`| `true` | | `allow_local_requests_from_system_hooks` | `false` | `boolean` | `boolean` | `true` | `true` | `false` | `false`| `true` | | `allow_local_requests_from_web_hooks_and_services` | `false` | `boolean` | `boolean` | `true` | `false` | `false` | `false`| `true` | diff --git a/doc/user/gitlab_com/_index.md b/doc/user/gitlab_com/_index.md index 6db6a93cfd7d0c96b96c1fa4894dbb5eb3236f83..376820aa23cc9db275440f844e6b5651a755f5d8 100644 --- a/doc/user/gitlab_com/_index.md +++ b/doc/user/gitlab_com/_index.md @@ -385,6 +385,7 @@ Settings related to the deletion of projects and groups. - Delayed group deletion enabled by default for GitLab Premium and GitLab Ultimate in GitLab 16.1. - [Moved](https://gitlab.com/groups/gitlab-org/-/epics/17208) from GitLab Premium to GitLab Free in 18.0. - [Increased deletion period](https://gitlab.com/groups/gitlab-org/-/epics/17375) from seven days to 30 days in 18.0.2. +- [Force-deletion was disabled](https://gitlab.com/groups/gitlab-org/-/epics/17375) in 18.4. {{< /history >}} @@ -399,6 +400,7 @@ See how to [view and restore groups marked for deletion](../group/_index.md#rest - Delayed project deletion enabled by default for GitLab Premium and GitLab Ultimate in GitLab 16.1. - [Moved](https://gitlab.com/groups/gitlab-org/-/epics/17208) from GitLab Premium to GitLab Free in 18.0. - [Increased deletion period](https://gitlab.com/groups/gitlab-org/-/epics/17375) from seven-days to 30 days in 18.0.2. +- [Force-deletion was disabled](https://gitlab.com/gitlab-org/gitlab/-/issues/562308) in 18.4. {{< /history >}} diff --git a/doc/user/group/_index.md b/doc/user/group/_index.md index 14190f2104a4f412c97de2cadc3e7cd0a7a7f7c4..0f09182e4b2c60dd12dad9047a345c0d663d3809 100644 --- a/doc/user/group/_index.md +++ b/doc/user/group/_index.md @@ -124,6 +124,34 @@ This page shows groups that you are a member of through: - Membership of a subgroup's parent group. - Direct or inherited membership of a project in the group or subgroup. +### View inactive groups + +{{< history >}} + +- **Inactive** tab [introduced](https://gitlab.com/groups/gitlab-org/-/epics/13781) in GitLab 18.2 [with a flag](../../administration/feature_flags/_index.md) named `your_work_groups_vue`. Disabled by default. +- [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/542790) in GitLab 18.3. Feature flag `your_work_groups_vue` removed. + +{{< /history >}} + +A group is inactive when: + +- It is pending deletion. +- It has been archived. + +To view all inactive groups: + +1. On the left sidebar, select **Search or go to** and find your group. +1. Select **View all my groups**. +1. Select the **Inactive** tab. + +Each group in the list shows: + +- A badge indicating that the group is archived or marked for deletion. + If the group is marked for deletion, the list also shows: + - The time the group was marked for deletion. + - The time the group is scheduled for final deletion. + - A **Restore** action to stop the group being eventually deleted. + ## View a group {{< history >}} @@ -249,6 +277,17 @@ To leave a group: {{< /history >}} +By default, when you delete a group for the first time, it enters a pending deletion state. +Delete a group again to remove it immediately. + +On GitLab.com, after a group is deleted, its data is retained for 30 days, and immediate deletion is not available. +If you really need to delete a group immediately on GitLab.com, you can open a [support ticket](https://about.gitlab.com/support/). + +Prerequisites: + +- You must have the Owner role for a group. +- If the groups contains any project, owners must be [allowed to delete projects](../../administration/settings/visibility_and_access_controls.md#restrict-project-deletion-to-administrators). + To delete a group and its contents: 1. On the left sidebar, select **Search or go to** and find your group. @@ -278,37 +317,24 @@ the deletion job instead restores the group, and the group is no longer schedule {{< /alert >}} -### View groups pending deletion - -{{< history >}} - -- **Inactive** tab [introduced](https://gitlab.com/groups/gitlab-org/-/epics/13781) in GitLab 18.2 [with a flag](../../administration/feature_flags/_index.md) named `your_work_groups_vue`. Disabled by default. -- [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/542790) in GitLab 18.3. Feature flag `your_work_groups_vue` removed. - -{{< /history >}} - -To view a list of the groups that are pending deletion: - -1. On the left sidebar, select **Search or go to** and find your group. -1. Select **View all my groups**. -1. Select the **Inactive** tab. - -Groups that are marked for deletion are labeled **Pending deletion**. - ## Delete a group immediately {{< history >}} - Enabled delayed deletion by default [on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/393622) and [on GitLab Self-Managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119606) in GitLab 16.0. - [Moved](https://gitlab.com/groups/gitlab-org/-/epics/17208) from GitLab Premium to GitLab Free in 18.0. -- Support for disallowing immediate deletion for groups or projects scheduled for deletion [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/201957) in GitLab 18.4 [with a flag](../../administration/feature_flags/_index.md) named `disallow_immediate_deletion`. Disabled by default. +- [Instance setting](../../administration/settings/visibility_and_access_controls.md#immediate-deletion) + to allow immediate deletion for groups or projects scheduled for deletion + [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/204669) in GitLab 18.5 + [with a flag](../../administration/feature_flags/_index.md) named `allow_immediate_namespaces_deletion`. + Enabled by default. Disabled on GitLab.com and Dedicated. {{< /history >}} -{{< alert type="flag" >}} +{{< alert type="warning" >}} -The availability of this feature is controlled by a feature flag. -For more information, see the history. +On GitLab.com, after a group is deleted, its data is retained for 30 days, and immediate deletion is not available. +If you really need to delete a group immediately on GitLab.com, you can open a [support ticket](https://about.gitlab.com/support/). {{< /alert >}} @@ -324,7 +350,7 @@ To immediately delete a group marked for deletion: 1. On the left sidebar, select **Search or go to** and find your group. 1. Select **Settings > General**. 1. Expand **Advanced**. -1. In the **Permanently delete group** section, select **Delete group**. +1. In the **Delete group immediately** section, select **Delete group immediately**. 1. Confirm the action when asked to. This action deletes the group, its subgroups, projects, and all related resources, including issues and merge requests. diff --git a/doc/user/project/working_with_projects.md b/doc/user/project/working_with_projects.md index 6001903151376044eec886dbc07204d57ddfa64e..729026ec1c6795f144d26cf12d80d4fc0e7cb7b4 100644 --- a/doc/user/project/working_with_projects.md +++ b/doc/user/project/working_with_projects.md @@ -330,26 +330,13 @@ To upload an avatar in your project settings: {{< history >}} - Default behavior [changed](https://gitlab.com/gitlab-org/gitlab/-/issues/389557) to delayed project deletion for Premium and Ultimate tiers on [GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/393622) and [GitLab Self-Managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119606) in 16.0. -- Option to delete projects immediately as a group setting removed [on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/393622) and [on GitLab Self-Managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119606) in GitLab 16.0. - Default behavior changed to delayed project deletion for [GitLab Free](https://gitlab.com/groups/gitlab-org/-/epics/17208) and [personal projects](https://gitlab.com/gitlab-org/gitlab/-/issues/536244) in 18.0. -- Option to delete projects immediately [moved](https://gitlab.com/groups/gitlab-org/-/epics/17208) from GitLab Premium to GitLab Free in 18.0. -- Support for disallowing immediate deletion for groups or projects scheduled for deletion [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/201957) in GitLab 18.4 [with a flag](../../administration/feature_flags/_index.md) named `disallow_immediate_deletion`. Disabled by default. {{< /history >}} -{{< alert type="flag" >}} - -The availability of this feature is controlled by a feature flag. -For more information, see the history. - -{{< /alert >}} - -You can schedule a project for deletion. By default, when you delete a project for the first time, it enters a pending deletion state. Delete a project again to remove it immediately. -On GitLab.com, after a project is deleted, its data is retained for 30 days. - Prerequisites: - You must have the Owner role for a project. @@ -362,14 +349,59 @@ To delete a project: 1. Expand **Advanced**. 1. In the **Delete project** section, select **Delete project**. 1. On the confirmation dialog, enter the project name and select **Yes, delete project**. -1. Optional. To delete the project immediately, repeat these steps. -You can also [delete projects using the Rails console](troubleshooting.md#delete-a-project-using-console). +This action adds a background job to mark a group for deletion. On GitLab.com, the group is deleted after 30 days. On GitLab Self-Managed, +you can modify the retention period through the [instance settings](../../administration/settings/visibility_and_access_controls.md#deletion-protection). If the user who scheduled the project deletion loses access to the project before the deletion occurs (for example, by leaving the project, having their role downgraded, or being banned from the project), -the deletion job restores the project. However, if the user regains access before the deletion job runs, -the job removes the project permanently. +the deletion job instead restores the project, and the project is no longer scheduled for deletion. + + {{< alert type="warning" >}} + + If the user who scheduled the project deletion regains Owner role or administrator access before the job runs, then the job removes the project permanently. + + {{< /alert >}} + +You can also [delete projects using the Rails console](troubleshooting.md#delete-a-project-using-console). + +## Delete a project immediately + +{{< history >}} + +- Option to delete projects immediately as a group setting removed [on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/393622) and [on GitLab Self-Managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119606) in GitLab 16.0. +- Option to delete projects immediately [moved](https://gitlab.com/groups/gitlab-org/-/epics/17208) from GitLab Premium to GitLab Free in 18.0. +- [Instance setting](../../administration/settings/visibility_and_access_controls.md#immediate-deletion) + to allow immediate deletion for groups or projects scheduled for deletion + [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/204669) in GitLab 18.5 + [with a flag](../../administration/feature_flags/_index.md) named `allow_immediate_namespaces_deletion`. + Enabled by default. Disabled on GitLab.com and Dedicated. + +{{< /history >}} + +{{< alert type="warning" >}} + +On GitLab.com, after a project is deleted, its data is retained for 30 days, and immediate deletion is not available. +If you really need to delete a project immediately on GitLab.com, you can open a [support ticket](https://about.gitlab.com/support/). + +{{< /alert >}} + +If you don't want to wait, you can delete a project immediately. + +Prerequisites: + +- You must have the Owner role for a project. +- You have [marked the project for deletion](#delete-a-project). + +To immediately delete a project marked for deletion: + +1. On the left sidebar, select **Search or go to** and find your project. +1. Select **Settings > General**. +1. Expand **Advanced**. +1. In the **Delete project immediately** section, select **Delete project immediately**. +1. Confirm the action when asked to. + +This action deletes the group, its subgroups, projects, and all related resources, including issues and merge requests. ### Restore a project @@ -432,7 +464,7 @@ This action is also available on other list pages. ### Unarchive a project -Unarchive a project to: +Unarchive a project to: - Remove read-only restrictions. - Return the project to the **Active** or **Member** tab in project lists. diff --git a/lib/api/groups.rb b/lib/api/groups.rb index ff048e6ee8e54255808561bfac94d09c4f950624..b96df3c60e796018422c5db13e854c4459f7e5bb 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -173,8 +173,8 @@ def present_groups_with_pagination_strategies(params, groups) end def immediately_delete_subgroup_error(group) - if Feature.enabled?(:disallow_immediate_deletion, current_user) - '`permanently_remove` option is not available anymore (behind the :disallow_immediate_deletion feature flag).' + if !Gitlab::CurrentSettings.allow_immediate_namespaces_deletion_for_user?(current_user) + '`permanently_remove` option is not permitted on this instance.' elsif !group.subgroup? '`permanently_remove` option is only available for subgroups.' elsif !group.self_deletion_scheduled? diff --git a/lib/api/projects.rb b/lib/api/projects.rb index ca862b486b072f50bfc2837c8295c9da6ccb7ac3..6270eda286cc81fb12187ecadc3c33f0fad75932 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -86,9 +86,8 @@ def support_order_by_similarity!(attrs) end def immediately_delete_project_error(project) - # Admin frontend uses this endpoint to force-delete projects - if Feature.enabled?(:disallow_immediate_deletion, current_user) && !current_user.can_admin_all_resources? - '`permanently_remove` option is not available anymore (behind the :disallow_immediate_deletion feature flag).' + if !Gitlab::CurrentSettings.allow_immediate_namespaces_deletion_for_user?(current_user) + '`permanently_remove` option is not permitted on this instance.' elsif !project.self_deletion_scheduled? 'Project must be marked for deletion first.' elsif project.full_path != params[:full_path] diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 15f090c3a05254e533f13e277e6ad4255b6996be..cf8e86b881424eea7bee9240b6a9a01f6594a0ba 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -20,6 +20,8 @@ def add_gon_variables gon.markdown_automatic_lists = current_user&.markdown_automatic_lists gon.markdown_maintain_indentation = current_user&.markdown_maintain_indentation gon.math_rendering_limits_enabled = Gitlab::CurrentSettings.math_rendering_limits_enabled + gon.allow_immediate_namespaces_deletion = + Gitlab::CurrentSettings.allow_immediate_namespaces_deletion_for_user?(current_user) add_browsersdk_tracking @@ -106,7 +108,6 @@ def add_gon_feature_flags push_frontend_feature_flag(:whats_new_featured_carousel) push_frontend_feature_flag(:extensible_reference_filters, current_user) push_frontend_feature_flag(:paneled_view, current_user) - push_frontend_feature_flag(:disallow_immediate_deletion, current_user) # Expose the Project Studio user preference as if it were a feature flag push_force_frontend_feature_flag(:project_studio_enabled, Users::ProjectStudio.new(current_user).enabled?) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f281cd5be01fc4c4be9dab7ba5586a0e89486db1..5a0d8048d748917507975f7e41e6d3374387d2e7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -22210,9 +22210,6 @@ msgstr "" msgid "Delete group" msgstr "" -msgid "Delete group immediately" -msgstr "" - msgid "Delete group immediately?" msgstr "" @@ -22240,9 +22237,6 @@ msgstr "" msgid "Delete project" msgstr "" -msgid "Delete project immediately" -msgstr "" - msgid "Delete release" msgstr "" @@ -32104,6 +32098,9 @@ msgstr "" msgid "GroupSelect|Select a group" msgstr "" +msgid "GroupSettings|%{link_start}How do I immediately delete a group?%{link_end}" +msgstr "" + msgid "GroupSettings|Add additional webhook triggers for group access token expiration" msgstr "" @@ -32182,6 +32179,12 @@ msgstr "" msgid "GroupSettings|Default to Auto DevOps pipeline for all projects within this group" msgstr "" +msgid "GroupSettings|Delete group" +msgstr "" + +msgid "GroupSettings|Delete group immediately" +msgstr "" + msgid "GroupSettings|Disable personal access tokens" msgstr "" @@ -32275,6 +32278,9 @@ msgstr "" msgid "GroupSettings|If the parent group's visibility is lower than the group's current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility." msgstr "" +msgid "GroupSettings|Immediate deletion is disabled" +msgstr "" + msgid "GroupSettings|Include diff previews" msgstr "" @@ -33838,6 +33844,15 @@ msgstr "" msgid "Images with incorrect dimensions are not resized automatically, and may result in unexpected behavior." msgstr "" +msgid "ImmediateNamespacesDeletionSettings|Allow immediate deletion" +msgstr "" + +msgid "ImmediateNamespacesDeletionSettings|Groups and projects marked for deletion can be immediately deleted with a second deletion action, bypassing the configured retention period. %{link_start}Learn more.%{link_end}" +msgstr "" + +msgid "ImmediateNamespacesDeletionSettings|Prevent non-administrators from immediately deleting groups and projects." +msgstr "" + msgid "Immutable tag rules require an Ultimate license" msgstr "" @@ -50872,6 +50887,9 @@ msgstr "" msgid "ProjectService|Trigger event when an issue is created, updated, or closed." msgstr "" +msgid "ProjectSettings|%{link_start}How do I immediately delete a project?%{link_end}" +msgstr "" + msgid "ProjectSettings|%{link_start}What are description templates?%{link_end}" msgstr "" @@ -50986,6 +51004,12 @@ msgstr "" msgid "ProjectSettings|Data sources" msgstr "" +msgid "ProjectSettings|Delete project" +msgstr "" + +msgid "ProjectSettings|Delete project immediately" +msgstr "" + msgid "ProjectSettings|Determine what happens to the commit history when you merge a merge request." msgstr "" @@ -51100,6 +51124,9 @@ msgstr "" msgid "ProjectSettings|If merge trains are enabled, merging is only possible if the branch can be rebased without conflicts." msgstr "" +msgid "ProjectSettings|Immediate deletion is disabled" +msgstr "" + msgid "ProjectSettings|Include diff previews" msgstr "" @@ -67221,6 +67248,12 @@ msgstr "" msgid "To continue, you need to select the link in the confirmation email we sent to verify your email address. If you didn't get our email, select %{strongStart}Resend confirmation email.%{strongEnd}" msgstr "" +msgid "To delete immediately this group, you must delete immediately its parent group." +msgstr "" + +msgid "To delete immediately this project, you must delete immediately its parent group." +msgstr "" + msgid "To disable the setting, set this value to 0." msgstr "" @@ -76666,6 +76699,9 @@ msgstr "" msgid "cannot be enabled because parent group has shared Runners disabled" msgstr "" +msgid "cannot be enabled on Dedicated" +msgstr "" + msgid "cannot be enabled unless all domains have TLS certificates" msgstr "" diff --git a/scripts/cells/application-settings-analysis.rb b/scripts/cells/application-settings-analysis.rb index 5c5fe6e8ab368d503a2b0b108051f3f01e41a7fc..7ba7866cc5154c94becada305013b7be5bbb5936 100755 --- a/scripts/cells/application-settings-analysis.rb +++ b/scripts/cells/application-settings-analysis.rb @@ -40,6 +40,7 @@ class ApplicationSetting < ApplicationSettingPrototype abuse_notification_email after_sign_out_path after_sign_up_text + allow_immediate_namespaces_deletion allow_top_level_group_owners_to_create_service_accounts arkose_labs_namespace asset_proxy_enabled diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 0f9d9a1504c87a69a04c4ba04fd4867376ad632c..598a845ed59b3bce8aca18e3208d205840ee03c5 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -633,7 +633,11 @@ context 'when permanently_remove param is set' do let(:params) { { permanently_remove: true } } - describe 'forbidden by the :disallow_immediate_deletion feature flag' do + describe 'when the :allow_immediate_namespaces_deletion application setting is false' do + before do + stub_application_setting(allow_immediate_namespaces_deletion: false) + end + it 'returns error' do Sidekiq::Testing.fake! do expect { subject }.not_to change { GroupDestroyWorker.jobs.size } @@ -656,32 +660,26 @@ end end - context 'when the :disallow_immediate_deletion feature flag is disabled' do - before do - stub_feature_flags(disallow_immediate_deletion: false) - end - - context 'for a html request' do - it 'deletes the group immediately and redirects to root path' do - expect(GroupDestroyWorker).to receive(:perform_async) + context 'for a html request' do + it 'deletes the group immediately and redirects to root path' do + expect(GroupDestroyWorker).to receive(:perform_async) - subject + subject - expect(response).to redirect_to(root_path) - expect(flash[:toast]).to include "Group '#{group.name}' is being deleted." - end + expect(response).to redirect_to(root_path) + expect(flash[:toast]).to include "Group '#{group.name}' is being deleted." end + end - context 'for a json request' do - let(:format) { :json } + context 'for a json request' do + let(:format) { :json } - it 'deletes the group immediately and returns json with message' do - expect(GroupDestroyWorker).to receive(:perform_async) + it 'deletes the group immediately and returns json with message' do + expect(GroupDestroyWorker).to receive(:perform_async) - subject + subject - expect(json_response['message']).to eq("Group '#{group.name}' is being deleted.") - end + expect(json_response['message']).to eq("Group '#{group.name}' is being deleted.") end end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 531f595dc304b622699d934458751fafc9e4f53a..36f3659e74353999d0eac30ddb748624de25aefb 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -1279,7 +1279,11 @@ def update_project_feature context 'when project is already marked for deletion' do let_it_be(:project) { create(:project, group: group, marked_for_deletion_at: Date.current) } - describe 'forbidden by the :disallow_immediate_deletion feature flag' do + describe 'when the :allow_immediate_namespaces_deletion application setting is false' do + before do + stub_application_setting(allow_immediate_namespaces_deletion: false) + end + subject(:request) { delete :destroy, params: { namespace_id: project.namespace, id: project, permanently_delete: true } } it 'returns error' do @@ -1291,21 +1295,15 @@ def update_project_feature end end - context 'when the :disallow_immediate_deletion feature flag is disabled' do - before do - stub_feature_flags(disallow_immediate_deletion: false) - end + context 'when permanently_delete param is set' do + it 'deletes project right away' do + expect(ProjectDestroyWorker).to receive(:perform_async) - context 'when permanently_delete param is set' do - it 'deletes project right away' do - expect(ProjectDestroyWorker).to receive(:perform_async) + delete :destroy, params: { namespace_id: project.namespace, id: project, permanently_delete: true } - delete :destroy, params: { namespace_id: project.namespace, id: project, permanently_delete: true } - - expect(project.reload.pending_delete).to eq(true) - expect(response).to have_gitlab_http_status(:found) - expect(response).to redirect_to(dashboard_projects_path) - end + expect(project.reload.pending_delete).to eq(true) + expect(response).to have_gitlab_http_status(:found) + expect(response).to redirect_to(dashboard_projects_path) end end diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index cec5c31ca65cf97ab74fa82ad530cbb4e6617558..d53a2957f9dbe76de12c271f0fe86a4f170b4fd4 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -50,6 +50,21 @@ end end + it 'change deletion settings', :js do + within_testid('admin-visibility-access-settings') do + fill_in 'Deletion protection', with: 30 + uncheck 'Allow immediate deletion' + click_button 'Save changes' + end + + expect(page).to have_content 'Application settings saved successfully' + + within_testid('admin-visibility-access-settings') do + expect(find_field('Deletion protection').value).to eq('30') + expect(find_field('Allow immediate deletion')).not_to be_checked + end + end + it 'modify import sources' do expect(current_settings.import_sources).to be_empty diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb index 69c8a002475a884e8bce2bfe18cc8fc2a5562a88..7c82395f11e5e06ad3402ff81a5fb01572813f07 100644 --- a/spec/features/groups/group_settings_spec.rb +++ b/spec/features/groups/group_settings_spec.rb @@ -402,20 +402,14 @@ def remove_with_confirm(button_text, confirm_with, confirm_button_text = 'Confir context 'when group is marked for deletion', :js do before do create(:group_deletion_schedule, group: group) - end - - context 'when the :disallow_immediate_deletion feature flag is disabled' do - before do - stub_feature_flags(disallow_immediate_deletion: false) - visit edit_group_path(group) - end + visit edit_group_path(group) + end - it 'deletes the project immediately', :sidekiq_inline do - expect { remove_with_confirm('Delete group immediately', group.path) }.to change { Group.count }.by(-1) + it 'deletes the project immediately', :sidekiq_inline do + expect { remove_with_confirm('Delete group immediately', group.path) }.to change { Group.count }.by(-1) - expect(page).to have_content "is being deleted" - end + expect(page).to have_content "is being deleted" end end end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 544709f3038065202d92c10386dbaba5b7cfb9ca..34d3a746f2a9cf025751e92ea738c7530bba7633 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -24,6 +24,7 @@ admin_mode: false, ai_action_api_rate_limit: 160, akismet_enabled: false, + allow_immediate_namespaces_deletion: true, allow_account_deletion: true, allow_bypass_placeholder_confirmation: false, allow_contribution_mapping_to_admins: false, @@ -1148,6 +1149,67 @@ def expect_invalid end end + describe '#allow_immediate_namespaces_deletion_for_user?' do + let(:user) { build_stubbed(:user) } + let(:admin) { build_stubbed(:admin) } + + before do + stub_application_setting(admin_mode: false) + end + + context 'with allow_immediate_namespaces_deletion disabled in database' do + before do + setting.update!(allow_immediate_namespaces_deletion: false) + end + + it { expect(setting.allow_immediate_namespaces_deletion_for_user?(user)).to be(false) } + + context 'when user is an admin' do + before do + puts "admin.can_admin_all_resources?: #{admin.can_admin_all_resources?}" + end + + it { expect(setting.allow_immediate_namespaces_deletion_for_user?(admin)).to be(true) } + end + + context 'when the :allow_immediate_namespaces_deletion feature flag is disabled' do + before do + stub_feature_flags(allow_immediate_namespaces_deletion: false) + end + + it { expect(setting.allow_immediate_namespaces_deletion_for_user?(user)).to be(true) } + + context 'when user is an admin' do + it { expect(setting.allow_immediate_namespaces_deletion_for_user?(admin)).to be(true) } + end + end + end + + context 'with allow_immediate_namespaces_deletion enabled in database' do + before do + setting.update!(allow_immediate_namespaces_deletion: true) + end + + it { expect(setting.allow_immediate_namespaces_deletion_for_user?(user)).to be(true) } + + context 'when user is an admin' do + it { expect(setting.allow_immediate_namespaces_deletion_for_user?(admin)).to be(true) } + end + + context 'when the :allow_immediate_namespaces_deletion feature flag is disabled' do + before do + stub_feature_flags(allow_immediate_namespaces_deletion: false) + end + + it { expect(setting.allow_immediate_namespaces_deletion_for_user?(user)).to be(true) } + + context 'when user is an admin' do + it { expect(setting.allow_immediate_namespaces_deletion_for_user?(admin)).to be(true) } + end + end + end + end + describe 'setting validated as `addressable_url` configured with external URI' do before do # Use any property that has the `addressable_url` validation. @@ -1806,6 +1868,26 @@ def expect_invalid ).for(:resource_access_tokens_settings) end end + + describe 'for allow_immediate_namespaces_deletion' do + context 'when on Dedicated' do + before do + stub_application_setting(gitlab_dedicated_instance: true) + end + + it { is_expected.to allow_value(false).for(:allow_immediate_namespaces_deletion) } + it { is_expected.not_to allow_value(true).for(:allow_immediate_namespaces_deletion) } + end + + context 'when not on Dedicated' do + before do + stub_application_setting(gitlab_dedicated_instance: false) + end + + it { is_expected.to allow_value(false).for(:allow_immediate_namespaces_deletion) } + it { is_expected.to allow_value(true).for(:allow_immediate_namespaces_deletion) } + end + end end describe 'callbacks' do diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 622bcb41c2da1501faae0ed8adc768e146f8d8e6..807e4b534795aedeb9b58e3eb3d1128249c3303c 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -3182,27 +3182,30 @@ def make_upload_request describe "DELETE /groups/:id" do let(:group) { group1 } let(:user) { user1 } + let(:admin_mode) { false } let(:params) { {} } - subject { delete api("/groups/#{group.id}", user), params: params } + subject(:api_request) { delete api("/groups/#{group.id}", user, admin_mode: admin_mode), params: params } shared_examples_for 'immediately enqueues the job to delete the group' do it 'immediately enqueues the job to delete the group', :clean_gitlab_redis_queues do Sidekiq::Testing.fake! do - expect { subject }.to change(GroupDestroyWorker.jobs, :size).by(1) + expect { api_request }.to change(GroupDestroyWorker.jobs, :size).by(1) end expect(response).to have_gitlab_http_status(:accepted) end end - shared_examples_for 'does not immediately enqueues the job to delete the group' do |error_message| + shared_examples_for 'does not immediately enqueues the job to delete the group' do + let(:expected_http_status) { :bad_request } + it 'does not immediately enqueues the job to delete the group', :clean_gitlab_redis_queues do Sidekiq::Testing.fake! do - expect { subject }.not_to change(GroupDestroyWorker.jobs, :size) + expect { api_request }.not_to change(GroupDestroyWorker.jobs, :size) end - expect(response).to have_gitlab_http_status(:bad_request) + expect(response).to have_gitlab_http_status(expected_http_status) expect(json_response['message']).to eq(error_message) end end @@ -3210,7 +3213,7 @@ def make_upload_request shared_examples_for 'marks group for delayed deletion' do it 'marks group for delayed deletion', :clean_gitlab_redis_queues do Sidekiq::Testing.fake! do - expect { subject }.not_to change(GroupDestroyWorker.jobs, :size) + expect { api_request }.not_to change(GroupDestroyWorker.jobs, :size) end group.reload @@ -3248,64 +3251,92 @@ def make_upload_request let(:params) { { permanently_remove: true } } context 'if group is a subgroup' do - let(:subgroup) { create(:group, parent: group) } + let(:parent_group) { create(:group) } - subject { delete api("/groups/#{subgroup.id}", user), params: params } - - context 'forbidden by the :disallow_immediate_deletion feature flag' do - it_behaves_like 'does not immediately enqueues the job to delete the group', - '`permanently_remove` option is not available anymore (behind the :disallow_immediate_deletion feature flag).' + before do + group.update!(parent: parent_group) end - context 'when the :disallow_immediate_deletion feature flag is disabled' do + describe 'when the :allow_immediate_namespaces_deletion application setting is false' do before do - stub_feature_flags(disallow_immediate_deletion: false) + stub_application_setting(allow_immediate_namespaces_deletion: false) end - context 'when group is not marked for deletion' do - it_behaves_like 'does not immediately enqueues the job to delete the group', 'Group must be marked for deletion first.' + it_behaves_like 'does not immediately enqueues the job to delete the group' do + let(:error_message) { '`permanently_remove` option is not permitted on this instance.' } end - context 'when group is already marked for deletion' do - before do - create(:group_deletion_schedule, group: subgroup, marked_for_deletion_on: Date.current) - end + context 'when current user is an admin' do + let_it_be(:user) { admin } + let(:admin_mode) { true } + + context 'when group is already marked for deletion' do + let(:params) { { permanently_remove: true, full_path: group.full_path } } - context 'when full_path param is not passed' do - it_behaves_like 'does not immediately enqueues the job to delete the group', - '`full_path` is incorrect. You must enter the complete path for the subgroup.' + before do + create(:group_deletion_schedule, group: group, marked_for_deletion_on: Date.current) + group.add_owner(admin) + end + + it_behaves_like 'immediately enqueues the job to delete the group' + + context 'when admin_mode is false' do + let(:admin_mode) { false } + + it_behaves_like 'does not immediately enqueues the job to delete the group' do + let(:error_message) { '`permanently_remove` option is not permitted on this instance.' } + end + end end + end + end - context 'when full_path param is not equal to full_path' do - let(:params) { { permanently_remove: true, full_path: subgroup.path } } + context 'when group is not marked for deletion' do + it_behaves_like 'does not immediately enqueues the job to delete the group' do + let(:error_message) { 'Group must be marked for deletion first.' } + end + end - it_behaves_like 'does not immediately enqueues the job to delete the group', - '`full_path` is incorrect. You must enter the complete path for the subgroup.' + context 'when group is already marked for deletion' do + before do + create(:group_deletion_schedule, group: group, marked_for_deletion_on: Date.current) + end + + context 'when full_path param is not passed' do + it_behaves_like 'does not immediately enqueues the job to delete the group' do + let(:error_message) { '`full_path` is incorrect. You must enter the complete path for the subgroup.' } end + end - context 'when the full_path param is passed and it matches the full path of subgroup' do - let(:params) { { permanently_remove: true, full_path: subgroup.full_path } } + context 'when full_path param is not equal to full_path' do + let(:params) { { permanently_remove: true, full_path: group.path } } - it_behaves_like 'immediately enqueues the job to delete the group' + it_behaves_like 'does not immediately enqueues the job to delete the group' do + let(:error_message) { '`full_path` is incorrect. You must enter the complete path for the subgroup.' } end end - end - end - context 'if group is not a subgroup' do - subject { delete api("/groups/#{group.id}", user), params: params } + context 'when the full_path param is passed and it matches the full path of subgroup' do + let(:params) { { permanently_remove: true, full_path: group.full_path } } - context 'forbidden by the :disallow_immediate_deletion feature flag' do - it_behaves_like 'does not immediately enqueues the job to delete the group', - '`permanently_remove` option is not available anymore (behind the :disallow_immediate_deletion feature flag).' + it_behaves_like 'immediately enqueues the job to delete the group' + end end + end - context 'when the :disallow_immediate_deletion feature flag is disabled' do + context 'if group is not a subgroup' do + describe 'when the :allow_immediate_namespaces_deletion application setting is false' do before do - stub_feature_flags(disallow_immediate_deletion: false) + stub_application_setting(allow_immediate_namespaces_deletion: false) end - it_behaves_like 'does not immediately enqueues the job to delete the group', '`permanently_remove` option is only available for subgroups.' + it_behaves_like 'does not immediately enqueues the job to delete the group' do + let(:error_message) { '`permanently_remove` option is not permitted on this instance.' } + end + end + + it_behaves_like 'does not immediately enqueues the job to delete the group' do + let(:error_message) { '`permanently_remove` option is only available for subgroups.' } end end end @@ -3317,7 +3348,7 @@ def make_upload_request end it 'returns an error' do - subject + api_request expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']).to eq('error') diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index e1249c941e69dc774a6a4a3773450be8789d04e7..ab2c2a632ad9a94802f05040723db704035e4386 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -5538,6 +5538,10 @@ def request describe 'DELETE /projects/:id' do let(:path) { "/projects/#{project.id}" } + let(:admin_mode) { false } + let(:params) { {} } + + subject(:api_request) { delete api(path, user, admin_mode: admin_mode), params: params } it_behaves_like 'DELETE request permissions for admin mode' do let(:success_status_code) { :accepted } @@ -5546,7 +5550,7 @@ def request context 'when authenticated as user' do it 'removes project' do - delete api(path, user) + api_request expect(response).to have_gitlab_http_status(:accepted) expect(json_response['message']).to eql('202 Accepted') @@ -5576,39 +5580,30 @@ def request end context 'when authenticated as admin' do - it 'removes any existing project' do - delete api("/projects/#{project.id}", admin, admin_mode: true) - - expect(response).to have_gitlab_http_status(:accepted) - expect(json_response['message']).to eql('202 Accepted') - end - - it 'does not remove a non existing project' do - delete api("/projects/#{non_existing_record_id}", admin, admin_mode: true) - expect(response).to have_gitlab_http_status(:not_found) - end - it_behaves_like '412 response' do let(:success_status) { 202 } - subject(:request) { api("/projects/#{project.id}", admin, admin_mode: true) } + subject(:request) { api(path, admin, admin_mode: true) } end end - shared_examples 'deletes project immediately' do |admin_mode = false| + shared_examples 'deletes project immediately' do it :aggregate_failures do expect(::Projects::DestroyService).to receive(:new).with(project, user, {}).and_call_original - delete api(path, user, admin_mode: admin_mode), params: params + api_request + expect(response).to have_gitlab_http_status(:accepted) end end - shared_examples 'immediately delete project error' do |admin_mode = false, expected_http_status = :bad_request| + shared_examples 'immediately delete project error' do + let(:expected_http_status) { :bad_request } + it :aggregate_failures do expect(::Projects::DestroyService).not_to receive(:new) expect(::Projects::MarkForDeletionService).not_to receive(:new) - delete api(path, user, admin_mode: admin_mode), params: params + api_request expect(response).to have_gitlab_http_status(expected_http_status) expect(Gitlab::Json.parse(response.body)['message']).to eq(error_message) @@ -5618,12 +5613,11 @@ def request context 'for delayed deletion' do let_it_be(:group) { create(:group) } let_it_be_with_reload(:project) { create(:project, group: group, owners: user) } - let(:params) { {} } it 'marks the project for deletion' do expect(::Projects::MarkForDeletionService).to receive(:new).with(project, user, {}).and_call_original - delete api(path, user), params: params + api_request expect(response).to have_gitlab_http_status(:accepted) expect(project.reload.self_deletion_scheduled?).to be_truthy @@ -5650,7 +5644,7 @@ def request message = 'Error' expect(::Projects::MarkForDeletionService).to receive_message_chain(:new, :execute).and_return({ status: :error, message: message }) - delete api("/projects/#{project.id}", user) + api_request expect(response).to have_gitlab_http_status(:bad_request) expect(json_response["message"]).to eq(message) @@ -5661,64 +5655,74 @@ def request params.merge!(permanently_remove: true) end - context 'forbidden by the :disallow_immediate_deletion feature flag' do - let(:error_message) { '`permanently_remove` option is not available anymore (behind the :disallow_immediate_deletion feature flag).' } + describe 'when the :allow_immediate_namespaces_deletion application setting is false' do + before do + stub_application_setting(allow_immediate_namespaces_deletion: false) + end - it_behaves_like 'immediately delete project error' - end + it_behaves_like 'immediately delete project error' do + let(:error_message) { '`permanently_remove` option is not permitted on this instance.' } + end - context 'when current user is an admin' do - let_it_be(:user) { admin } + context 'when current user is an admin' do + let_it_be(:user) { admin } + let(:admin_mode) { true } - context 'when project is already marked for deletion' do - before do - project.update!(archived: true, marked_for_deletion_at: 1.day.ago, deleting_user: user) - params.merge!(full_path: project.full_path) - end + context 'when project is already marked for deletion' do + before do + project.update!(marked_for_deletion_at: 1.day.ago, deleting_user: admin) + params[:full_path] = project.full_path + project.add_owner(admin) + end - it_behaves_like 'deletes project immediately', true + it_behaves_like 'deletes project immediately' - context 'when admin_mode is false' do - let(:error_message) { '404 Project Not Found' } + context 'when admin_mode is false' do + let(:admin_mode) { false } - it_behaves_like 'immediately delete project error', false, :not_found + it_behaves_like 'immediately delete project error' do + let(:error_message) { '`permanently_remove` option is not permitted on this instance.' } + end + end end end end - context 'when the :disallow_immediate_deletion feature flag is disabled' do - before do - stub_feature_flags(disallow_immediate_deletion: false) - end + context 'when project is not marked for deletion' do + let(:error_message) { 'Project must be marked for deletion first.' } - context 'when project is not marked for deletion' do - let(:error_message) { 'Project must be marked for deletion first.' } + it_behaves_like 'immediately delete project error' + end - it_behaves_like 'immediately delete project error' + context 'when project is already marked for deletion' do + before do + project.update!(archived: true, marked_for_deletion_at: 1.day.ago, deleting_user: user) end - context 'when project is already marked for deletion' do + context 'with correct project full path' do before do - project.update!(archived: true, marked_for_deletion_at: 1.day.ago, deleting_user: user) + params.merge!(full_path: project.full_path) end - context 'with correct project full path' do + it_behaves_like 'deletes project immediately' + + context 'when the allow_immediate_namespaces_deletion FF is disabled' do before do - params.merge!(full_path: project.full_path) + stub_feature_flags(allow_immediate_namespaces_deletion: false) end it_behaves_like 'deletes project immediately' end + end - context 'with incorrect project full path' do - let(:error_message) { '`full_path` is incorrect. You must enter the complete path for the project.' } - - before do - params.merge!(full_path: "#{project.full_path}-wrong-path") - end + context 'with incorrect project full path' do + let(:error_message) { '`full_path` is incorrect. You must enter the complete path for the project.' } - it_behaves_like 'immediately delete project error' + before do + params.merge!(full_path: "#{project.full_path}-wrong-path") end + + it_behaves_like 'immediately delete project error' end end end diff --git a/spec/requests/organizations/groups_controller_spec.rb b/spec/requests/organizations/groups_controller_spec.rb index 9d7ba5a2829d5db36b2a468fdc6e252f66072ae1..7cba1f158a7374779969c71f545d5a861520d2e8 100644 --- a/spec/requests/organizations/groups_controller_spec.rb +++ b/spec/requests/organizations/groups_controller_spec.rb @@ -308,7 +308,11 @@ delete groups_organization_path(organization, id: group.to_param), params: params, as: :json end - describe 'forbidden by the :disallow_immediate_deletion feature flag' do + describe 'when the :allow_immediate_namespaces_deletion application setting is false' do + before do + stub_application_setting(allow_immediate_namespaces_deletion: false) + end + it 'returns error' do Sidekiq::Testing.fake! do expect { gitlab_request }.not_to change { GroupDestroyWorker.jobs.size } @@ -318,19 +322,13 @@ end end - context 'when the :disallow_immediate_deletion feature flag is disabled' do - before do - stub_feature_flags(disallow_immediate_deletion: false) - end - - it 'deletes the group immediately' do - expect(GroupDestroyWorker).to receive(:perform_async) + it 'deletes the group immediately' do + expect(GroupDestroyWorker).to receive(:perform_async) - gitlab_request + gitlab_request - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['message']).to include "Group '#{group.name}' is being deleted." - end + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['message']).to include "Group '#{group.name}' is being deleted." end end diff --git a/spec/views/admin/application_settings/_deletion_protection_settings.html.haml_spec.rb b/spec/views/admin/application_settings/_deletion_protection_settings.html.haml_spec.rb deleted file mode 100644 index 0ab746ff30f9d3bc61da92d1ac5b61a0ee9c01d4..0000000000000000000000000000000000000000 --- a/spec/views/admin/application_settings/_deletion_protection_settings.html.haml_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'admin/application_settings/_deletion_protection_settings', feature_category: :system_access do - let_it_be(:application_setting) do - build( - :application_setting, - deletion_adjourned_period: 1 - ) - end - - before do - assign(:application_setting, application_setting) - end - - it 'renders the deletion protection settings app root' do - render - - expect(rendered).to have_selector('#js-admin-deletion-protection-settings') - end -end diff --git a/spec/views/admin/application_settings/general.html.haml_spec.rb b/spec/views/admin/application_settings/general.html.haml_spec.rb index b5b4ead41a248d996d5e75f3f0b12aeced28fd0c..39b336391bbe8852c007aed08fcf4cc7f1d643dd 100644 --- a/spec/views/admin/application_settings/general.html.haml_spec.rb +++ b/spec/views/admin/application_settings/general.html.haml_spec.rb @@ -11,6 +11,15 @@ allow(view).to receive(:current_user).and_return(user) end + describe 'deletion protection settings' do + it 'renders the deletion protection settings' do + render + + expect(rendered).to have_selector('#js-admin-deletion-protection-settings') + expect(rendered).to have_field('Allow immediate deletion') + end + end + describe 'sourcegraph integration' do context 'when sourcegraph feature is enabled' do it 'show the form' do