diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 6f15bc553bfd4c302b2591689e610968e74f96fa..cee56dca538045fed252543714001b6f52c3e564 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -29,7 +29,8 @@ class ProjectsController < Projects::ApplicationController
before_action :authorize_read_code!, only: [:refs]
# Authorize
- before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export]
+ before_action :authorize_admin_project_or_custom_permissions!, only: :edit
+ before_action :authorize_admin_project!, only: [:update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export]
before_action :authorize_archive_project!, only: [:archive, :unarchive]
before_action :event_filter, only: [:show, :activity]
@@ -598,6 +599,11 @@ def check_export_rate_limit!
def render_edit
render 'edit'
end
+
+ # Overridden in EE
+ def authorize_admin_project_or_custom_permissions!
+ authorize_admin_project!
+ end
end
ProjectsController.prepend_mod_with('ProjectsController')
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 4e84a6ef7e75c69ffe1f0af47f98e8812b30e01a..fd0dc1178f7754fbff99de8337808303ee5d18b6 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -5,116 +5,119 @@
- reduce_visibility_form_id = 'reduce-visibility-form'
- @force_desktop_expanded_sidebar = true
-= render Pajamas::AlertComponent.new(title: _('GitLab Pages has moved'),
+- if can?(current_user, :admin_project, @project)
+ = render Pajamas::AlertComponent.new(title: _('GitLab Pages has moved'),
alert_options: { class: 'gl-my-5', data: { feature_id: Users::CalloutsHelper::PAGES_MOVED_CALLOUT, dismiss_endpoint: callouts_path, defer_links: 'true' } }) do |c|
- - c.with_body do
- = _('To go to GitLab Pages, on the left sidebar, select %{pages_link}.').html_safe % {pages_link: link_to('Deploy > Pages', project_pages_path(@project)).html_safe}
-
-%section.settings.general-settings.no-animate.expanded#js-general-settings
- .settings-header
- %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Naming, topics, avatar')
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
- = _('Collapse')
- %p.gl-text-secondary= _('Update your project name, topics, description, and avatar.')
- .settings-content= render 'projects/settings/general'
-
-%section.settings.sharing-permissions.no-animate#js-shared-permissions{ class: ('expanded' if expanded), data: { testid: 'visibility-features-permissions-content' } }
- .settings-header
- %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Visibility, project features, permissions')
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
- = expanded ? _('Collapse') : _('Expand')
- %p.gl-text-secondary= _('Choose visibility level, enable/disable project features and their permissions, disable email notifications, and show default emoji reactions.')
-
- .settings-content
- = form_for @project, html: { multipart: true, class: "sharing-permissions-form", id: reduce_visibility_form_id }, authenticity_token: true do |f|
- %input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' }
- %template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project).to_json.html_safe
- .js-project-permissions-form{ data: visibility_confirm_modal_data(@project, reduce_visibility_form_id) }
-- if show_merge_request_settings_callout?(@project)
- %section.settings.expanded
- = render Pajamas::AlertComponent.new(variant: :info,
+ - c.with_body do
+ = _('To go to GitLab Pages, on the left sidebar, select %{pages_link}.').html_safe % {pages_link: link_to('Deploy > Pages', project_pages_path(@project)).html_safe}
+
+ %section.settings.general-settings.no-animate.expanded#js-general-settings
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Naming, topics, avatar')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = _('Collapse')
+ %p.gl-text-secondary= _('Update your project name, topics, description, and avatar.')
+ .settings-content= render 'projects/settings/general'
+
+ %section.settings.sharing-permissions.no-animate#js-shared-permissions{ class: ('expanded' if expanded), data: { testid: 'visibility-features-permissions-content' } }
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Visibility, project features, permissions')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
+ %p.gl-text-secondary= _('Choose visibility level, enable/disable project features and their permissions, disable email notifications, and show default emoji reactions.')
+
+ .settings-content
+ = form_for @project, html: { multipart: true, class: "sharing-permissions-form", id: reduce_visibility_form_id }, authenticity_token: true do |f|
+ %input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' }
+ %template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project).to_json.html_safe
+ .js-project-permissions-form{ data: visibility_confirm_modal_data(@project, reduce_visibility_form_id) }
+ - if show_merge_request_settings_callout?(@project)
+ %section.settings.expanded
+ = render Pajamas::AlertComponent.new(variant: :info,
title: _('Merge requests and approvals settings have moved.'),
alert_options: { class: 'js-merge-request-settings-callout gl-my-5', data: { feature_id: Users::CalloutsHelper::MERGE_REQUEST_SETTINGS_MOVED_CALLOUT, dismiss_endpoint: callouts_path, defer_links: 'true' } }) do |c|
- - c.with_body do
- = _('On the left sidebar, select %{merge_requests_link} to view them.').html_safe % { merge_requests_link: link_to('Settings > Merge requests', project_settings_merge_requests_path(@project)).html_safe }
-
-%section.settings.no-animate{ class: ('expanded' if expanded), data: { testid: 'badges-settings-content' } }
- .settings-header
- %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
- = s_('ProjectSettings|Badges')
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
- = expanded ? _('Collapse') : _('Expand')
- %p.gl-text-secondary
- = s_('ProjectSettings|Customize this project\'s badges.')
- = link_to s_('ProjectSettings|What are badges?'), help_page_path('user/project/badges')
- .settings-content
- = render 'shared/badges/badge_settings'
-
-= render_if_exists 'compliance_management/compliance_framework/project_settings', expanded: expanded
-
-= render_if_exists 'projects/settings/default_issue_template'
-
-= render 'projects/service_desk_settings'
-
-%section.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded), data: { testid: 'advanced-settings-content' } }
- .settings-header
- %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Advanced')
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
- = expanded ? _('Collapse') : _('Expand')
- %p.gl-text-secondary= s_('ProjectSettings|Housekeeping, export, archive, change path, transfer, and delete.')
-
- .settings-content
- = render_if_exists 'projects/settings/restore', project: @project
-
- = render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card gl-mt-0' }, header_options: { class: 'gl-new-card-header gl-flex-direction-column' }, body_options: { class: 'gl-new-card-body gl-px-5 gl-py-4' }) do |c|
- - c.with_header do
- .gl-new-card-title-wrapper
- %h4.gl-new-card-title= _('Housekeeping')
- %p.gl-new-card-description
- = _('Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects.')
- = link_to _('Learn more.'), help_page_path('administration/housekeeping'), target: '_blank', rel: 'noopener noreferrer'
-
- - c.with_body do
- .gl-display-flex.gl-flex-wrap.gl-gap-3
- = render Pajamas::ButtonComponent.new(method: :post, href: housekeeping_project_path(@project)) do
- = _('Run housekeeping')
- #js-project-prune-unreachable-objects-button{ data: { prune_objects_path: housekeeping_project_path(@project, prune: true), prune_objects_doc_path: help_page_path('administration/housekeeping', anchor: 'prune-unreachable-objects') } }
-
- = render 'export', project: @project
-
- = render_if_exists 'projects/settings/archive'
-
- = render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card rename-repository' }, header_options: { class: 'gl-new-card-header gl-flex-direction-column' }, body_options: { class: 'gl-new-card-body gl-px-5 gl-py-4' }) do |c|
- - c.with_header do
- .gl-new-card-title-wrapper
- %h4.gl-new-card-title.warning-title= _('Change path')
- %p.gl-new-card-description
- - link = link_to('', help_page_path('user/project/settings/index', anchor: 'rename-a-repository'), target: '_blank', rel: 'noopener noreferrer')
- = safe_format(_("A project’s repository name defines its URL (the one you use to access the project via a browser) and its place on the file disk where GitLab is installed. %{link_start}Learn more.%{link_end}"), tag_pair(link, :link_start, :link_end))
-
- - c.with_body do
- = render 'projects/errors'
- = gitlab_ui_form_for @project do |f|
- .form-group
- %p
- %span.gl-font-weight-bold= _("Be careful. Renaming a project's repository can have unintended side effects.")
- = _('You will need to update your local repositories to point to the new location.')
- - if @project.deployment_platform.present?
- %p= _('Your deployment services will be broken, you will need to manually fix the services after renaming.')
- = f.label :path, _('Path'), class: 'label-bold'
+ - c.with_body do
+ = _('On the left sidebar, select %{merge_requests_link} to view them.').html_safe % { merge_requests_link: link_to('Settings > Merge requests', project_settings_merge_requests_path(@project)).html_safe }
+
+ %section.settings.no-animate{ class: ('expanded' if expanded), data: { testid: 'badges-settings-content' } }
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
+ = s_('ProjectSettings|Badges')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
+ %p.gl-text-secondary
+ = s_('ProjectSettings|Customize this project\'s badges.')
+ = link_to s_('ProjectSettings|What are badges?'), help_page_path('user/project/badges')
+ .settings-content
+ = render 'shared/badges/badge_settings'
+
+ = render_if_exists 'compliance_management/compliance_framework/project_settings', expanded: expanded
+
+ = render_if_exists 'projects/settings/default_issue_template'
+
+ = render 'projects/service_desk_settings'
+
+ %section.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded), data: { testid: 'advanced-settings-content' } }
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Advanced')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
+ %p.gl-text-secondary= s_('ProjectSettings|Housekeeping, export, archive, change path, transfer, and delete.')
+
+ .settings-content
+ = render_if_exists 'projects/settings/restore', project: @project
+
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card gl-mt-0' }, header_options: { class: 'gl-new-card-header gl-flex-direction-column' }, body_options: { class: 'gl-new-card-body gl-px-5 gl-py-4' }) do |c|
+ - c.with_header do
+ .gl-new-card-title-wrapper
+ %h4.gl-new-card-title= _('Housekeeping')
+ %p.gl-new-card-description
+ = _('Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects.')
+ = link_to _('Learn more.'), help_page_path('administration/housekeeping'), target: '_blank', rel: 'noopener noreferrer'
+
+ - c.with_body do
+ .gl-display-flex.gl-flex-wrap.gl-gap-3
+ = render Pajamas::ButtonComponent.new(method: :post, href: housekeeping_project_path(@project)) do
+ = _('Run housekeeping')
+ #js-project-prune-unreachable-objects-button{ data: { prune_objects_path: housekeeping_project_path(@project, prune: true), prune_objects_doc_path: help_page_path('administration/housekeeping', anchor: 'prune-unreachable-objects') } }
+
+ = render 'export', project: @project
+
+ = render_if_exists 'projects/settings/archive'
+
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card rename-repository' }, header_options: { class: 'gl-new-card-header gl-flex-direction-column' }, body_options: { class: 'gl-new-card-body gl-px-5 gl-py-4' }) do |c|
+ - c.with_header do
+ .gl-new-card-title-wrapper
+ %h4.gl-new-card-title.warning-title= _('Change path')
+ %p.gl-new-card-description
+ - link = link_to('', help_page_path('user/project/settings/index', anchor: 'rename-a-repository'), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(_("A project’s repository name defines its URL (the one you use to access the project via a browser) and its place on the file disk where GitLab is installed. %{link_start}Learn more.%{link_end}"), tag_pair(link, :link_start, :link_end))
+
+ - c.with_body do
+ = render 'projects/errors'
+ = gitlab_ui_form_for @project do |f|
.form-group
- .input-group
- .input-group-prepend
- .input-group-text
- #{Gitlab::Utils.append_path(root_url, @project.namespace.full_path)}/
- = f.text_field :path, class: 'form-control gl-form-input-xl', data: { testid: 'project-path-field' }
- = f.submit _('Change path'), class: "btn-danger", data: { testid: 'change-path-button' }, pajamas_button: true
-
- = render 'transfer', project: @project
-
- = render 'remove_fork', project: @project
-
- = render 'remove', project: @project
+ %p
+ %span.gl-font-weight-bold= _("Be careful. Renaming a project's repository can have unintended side effects.")
+ = _('You will need to update your local repositories to point to the new location.')
+ - if @project.deployment_platform.present?
+ %p= _('Your deployment services will be broken, you will need to manually fix the services after renaming.')
+ = f.label :path, _('Path'), class: 'label-bold'
+ .form-group
+ .input-group
+ .input-group-prepend
+ .input-group-text
+ #{Gitlab::Utils.append_path(root_url, @project.namespace.full_path)}/
+ = f.text_field :path, class: 'form-control gl-form-input-xl', data: { testid: 'project-path-field' }
+ = f.submit _('Change path'), class: "btn-danger", data: { testid: 'change-path-button' }, pajamas_button: true
+
+ = render 'transfer', project: @project
+
+ = render 'remove_fork', project: @project
+
+ = render 'remove', project: @project
+- elsif can?(current_user, :archive_project, @project)
+ = render_if_exists 'projects/settings/archive'
.save-project-loader.hide
.center
diff --git a/db/migrate/20231024123444_add_archive_project_to_member_roles.rb b/db/migrate/20231024123444_add_archive_project_to_member_roles.rb
new file mode 100644
index 0000000000000000000000000000000000000000..27ff86450e83cf26034f3384c3e6e2007a538c0f
--- /dev/null
+++ b/db/migrate/20231024123444_add_archive_project_to_member_roles.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddArchiveProjectToMemberRoles < Gitlab::Database::Migration[2.1]
+ enable_lock_retries!
+
+ def change
+ add_column :member_roles, :archive_project, :boolean, default: false, null: false
+ end
+end
diff --git a/db/schema_migrations/20231024123444 b/db/schema_migrations/20231024123444
new file mode 100644
index 0000000000000000000000000000000000000000..578f1cef1bdca9890283de1383b291ceee6ac27b
--- /dev/null
+++ b/db/schema_migrations/20231024123444
@@ -0,0 +1 @@
+db84d40c9afd9121aa24617167fa82b86cabc98bf274e61057eef02e1fafd7c3
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index ab1bf35fc90dce8063b66699d4578e824283a8fa..a75b1bbe8f8b82729e33ee3c11b9e383ddd7d3ed 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -18525,6 +18525,7 @@ CREATE TABLE member_roles (
admin_merge_request boolean DEFAULT false NOT NULL,
admin_group_member boolean DEFAULT false NOT NULL,
manage_project_access_tokens boolean DEFAULT false NOT NULL,
+ archive_project boolean DEFAULT false NOT NULL,
CONSTRAINT check_4364846f58 CHECK ((char_length(description) <= 255)),
CONSTRAINT check_9907916995 CHECK ((char_length(name) <= 255))
);
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 1b83954a79432202035e8c68ad8f8baa5f30e681..9600020e8036b7c7d39544f05fdcb7f8ae880c28 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -5133,6 +5133,7 @@ Input type: `MemberRoleCreateInput`
| `adminGroupMember` | [`Boolean`](#boolean) | Permission to admin group members. |
| `adminMergeRequest` | [`Boolean`](#boolean) | Permission to admin merge requests. |
| `adminVulnerability` | [`Boolean`](#boolean) | Permission to admin vulnerability. |
+| `archiveProject` | [`Boolean`](#boolean) | Permission to archive projects. |
| `baseAccessLevel` | [`MemberAccessLevel!`](#memberaccesslevel) | Base access level for the custom role. |
| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| `description` | [`String`](#string) | Description of the member role. |
@@ -20321,6 +20322,7 @@ Represents a member role.
| `adminGroupMember` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Permission to admin group members. |
| `adminMergeRequest` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Permission to admin merge requests. |
| `adminVulnerability` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Permission to admin vulnerability. |
+| `archiveProject` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 16.6. This feature is an Experiment. It can be changed or removed at any time. Permission to archive projects. |
| `baseAccessLevel` **{warning-solid}** | [`AccessLevel!`](#accesslevel) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Base access level for the custom role. |
| `description` | [`String`](#string) | Description of the member role. |
| `enabledPermissions` **{warning-solid}** | [`[MemberRolePermission!]`](#memberrolepermission) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Array of all permissions enabled for the custom role. |
@@ -29405,6 +29407,7 @@ Member role permission.
| `ADMIN_GROUP_MEMBER` | Allows admin access to group members. |
| `ADMIN_MERGE_REQUEST` | Allows admin access to the merge requests. |
| `ADMIN_VULNERABILITY` | Allows admin access to the vulnerability reports. |
+| `ARCHIVE_PROJECT` | Allows to archive projects. |
| `MANAGE_PROJECT_ACCESS_TOKENS` | Allows manage access to the project access tokens. |
| `READ_CODE` | Allows read-only access to the source code. |
| `READ_DEPENDENCY` | Allows read-only access to the dependencies. |
diff --git a/doc/api/member_roles.md b/doc/api/member_roles.md
index cc50a8e225a339ab306583d4bc443cf6ba91d332..63de583de25614073d8db2fb7fa2d4073f7610af 100644
--- a/doc/api/member_roles.md
+++ b/doc/api/member_roles.md
@@ -13,12 +13,14 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Read dependency added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126247) in GitLab 16.3.
> - [Name and description fields added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126423) in GitLab 16.3.
> - [Admin merge request introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128302) in GitLab 16.4 [with a flag](../administration/feature_flags.md) named `admin_merge_request`. Disabled by default.
+> - [Feature flag `admin_merge_request` removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132578) in GitLab 16.5.
> - [Admin group members introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131914) in GitLab 16.5 [with a flag](../administration/feature_flags.md) named `admin_group_member`. Disabled by default. The feature flag has been removed in GitLab 16.6.
> - [Manage project access tokens introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132342) in GitLab 16.5 in [with a flag](../administration/feature_flags.md) named `manage_project_access_tokens`. Disabled by default.
+> - [Archive project introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134998) in GitLab 16.6 in [with a flag](../administration/feature_flags.md) named `archive_project`. Disabled by default.
FLAG:
-On self-managed GitLab, by default these two features are not available. To make them available, an administrator can [enable the feature flags](../administration/feature_flags.md) named `admin_merge_request` and `admin_member_custom_role`.
-On GitLab.com, this feature is not available.
+On self-managed GitLab, by default these features are not available. To make them available, an administrator can [enable the feature flags](../administration/feature_flags.md) named `admin_group_member`, `manage_project_access_tokens` and `archive_project`.
+On GitLab.com, these features are not available.
## List all member roles of a group
@@ -48,6 +50,7 @@ If successful, returns [`200`](rest/index.md#status-codes) and the following res
| `[].read_vulnerability` | boolean | Permission to read project vulnerabilities. |
| `[].admin_group_member` | boolean | Permission to admin members of a group. |
| `[].manage_project_access_tokens` | boolean | Permission to manage project access tokens. |
+| `[].archive_project` | boolean | Permission to archive projects. |
Example request:
@@ -70,7 +73,8 @@ Example response:
"read_code": true,
"read_dependency": false,
"read_vulnerability": false,
- "manage_project_access_tokens": false
+ "manage_project_access_tokens": false,
+ "archive_project": false
},
{
"id": 3,
@@ -83,7 +87,8 @@ Example response:
"read_code": false,
"read_dependency": true,
"read_vulnerability": true,
- "manage_project_access_tokens": false
+ "manage_project_access_tokens": false,
+ "archive_project": false
}
]
```
diff --git a/doc/user/custom_roles.md b/doc/user/custom_roles.md
index 1b827d8279296a80b6e4d5f799938a88cc041c60..bbb487240782efbb1a4d09ccf9a47ff741b89ea8 100644
--- a/doc/user/custom_roles.md
+++ b/doc/user/custom_roles.md
@@ -15,6 +15,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - Ability to create and remove a custom role with the UI [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393235) in GitLab 16.4.
> - Ability to manage group members [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17364) in GitLab 16.5.
> - Ability to manage project access tokens [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/421778) in GitLab 16.5 [with a flag](../administration/feature_flags.md) named `manage_project_access_tokens`.
+> - Ability to archive projects [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/425957) in GitLab 16.6 in [with a flag](../administration/feature_flags.md) named `archive_project`. Disabled by default.
Custom roles allow group Owners or instance administrators to create roles
specific to the needs of their organization.
@@ -97,6 +98,7 @@ These requirements are documented in the `Required permission` column in the fol
| `admin_merge_request` | GitLab 16.4 and later | Not applicable | View and approve [merge requests](project/merge_requests/index.md), and view the associated merge request code.
Does not allow users to view or change merge request approval rules. |
| `manage_project_access_tokens` | GitLab 16.5 and later | Not applicable | Create, delete, and list [project access tokens](project/settings/project_access_tokens.md). |
| `admin_group_member` | GitLab 16.5 and later | Not applicable | Add or remove [group members](group/manage.md). |
+| `archive_project` | GitLab 16.6 and later | Not applicable | Archive and unarchive [projects](project/settings/index.md#archive-a-project). |
## Billing and seat usage
diff --git a/ee/app/assets/javascripts/roles_and_permissions/components/create_member_role.vue b/ee/app/assets/javascripts/roles_and_permissions/components/create_member_role.vue
index 0a37c86bf6886acdaf4c7eaaad178233e4c49acd..3ca33a4759c943546f0e3dd40a7050836c21c83c 100644
--- a/ee/app/assets/javascripts/roles_and_permissions/components/create_member_role.vue
+++ b/ee/app/assets/javascripts/roles_and_permissions/components/create_member_role.vue
@@ -62,9 +62,16 @@ export default {
},
computed: {
selectablePermissions() {
- return gon.features.manageProjectAccessTokens
- ? this.availablePermissions
- : this.availablePermissions.filter(({ value }) => value !== 'manage_project_access_tokens');
+ return this.availablePermissions.filter(({ value }) => {
+ switch (value) {
+ case 'manage_project_access_tokens':
+ return Boolean(gon.features.manageProjectAccessTokens);
+ case 'archive_project':
+ return Boolean(gon.features.archiveProject);
+ default:
+ return true;
+ }
+ });
},
},
methods: {
diff --git a/ee/app/assets/javascripts/roles_and_permissions/components/list_member_roles.vue b/ee/app/assets/javascripts/roles_and_permissions/components/list_member_roles.vue
index 08420e117443f3075960082431f46314ac9c5bc7..80a483255198ae8955344ed0155c42ba75525939 100644
--- a/ee/app/assets/javascripts/roles_and_permissions/components/list_member_roles.vue
+++ b/ee/app/assets/javascripts/roles_and_permissions/components/list_member_roles.vue
@@ -218,7 +218,7 @@ export default {
diff --git a/ee/app/controllers/ee/projects_controller.rb b/ee/app/controllers/ee/projects_controller.rb
index b9c2c63c5d5aaa8804a2547ee8d649d4aa87e0a6..a66458da13ac310944c618922938c6ba35b54ce1 100644
--- a/ee/app/controllers/ee/projects_controller.rb
+++ b/ee/app/controllers/ee/projects_controller.rb
@@ -222,5 +222,9 @@ def log_archive_audit_event
def log_unarchive_audit_event
log_audit_event(message: 'Project unarchived', event_type: 'project_unarchived')
end
+
+ def authorize_admin_project_or_custom_permissions!
+ can?(current_user, :archive_project, project) || super
+ end
end
end
diff --git a/ee/app/controllers/groups/settings/roles_and_permissions_controller.rb b/ee/app/controllers/groups/settings/roles_and_permissions_controller.rb
index 6ed3c81fdeba7462e5ebf193a0913a49f8ddc546..ec5d95865f16f46a0c6f53ef9f0514b7ac50f360 100644
--- a/ee/app/controllers/groups/settings/roles_and_permissions_controller.rb
+++ b/ee/app/controllers/groups/settings/roles_and_permissions_controller.rb
@@ -10,6 +10,7 @@ class RolesAndPermissionsController < Groups::ApplicationController
before_action :ensure_custom_roles_available!
before_action do
push_frontend_feature_flag(:manage_project_access_tokens, group)
+ push_frontend_feature_flag(:archive_project, group)
end
private
diff --git a/ee/app/graphql/mutations/member_roles/create.rb b/ee/app/graphql/mutations/member_roles/create.rb
index c6c8a62218acad542f272fae569616a0c48a9ed3..297d2f868bb174f4b999a81512e71f1102d0ab30 100644
--- a/ee/app/graphql/mutations/member_roles/create.rb
+++ b/ee/app/graphql/mutations/member_roles/create.rb
@@ -21,6 +21,10 @@ class Create < Base
GraphQL::Types::Boolean,
required: false,
description: 'Permission to admin vulnerability.'
+ argument :archive_project,
+ GraphQL::Types::Boolean,
+ required: false,
+ description: 'Permission to archive projects.'
argument :base_access_level,
::Types::MemberAccessLevelEnum,
required: true,
diff --git a/ee/app/graphql/types/member_roles/member_role_type.rb b/ee/app/graphql/types/member_roles/member_role_type.rb
index 7fed4e8b651685ea1f6947952dd187f2e8f565ee..de5fecfdfb09568a65b9602bceda5d91e30b2a0c 100644
--- a/ee/app/graphql/types/member_roles/member_role_type.rb
+++ b/ee/app/graphql/types/member_roles/member_role_type.rb
@@ -41,6 +41,12 @@ class MemberRoleType < BaseObject
alpha: { milestone: '16.5' },
description: 'Permission to admin group members.'
+ field :archive_project,
+ GraphQL::Types::Boolean,
+ null: true,
+ alpha: { milestone: '16.6' },
+ description: 'Permission to archive projects.'
+
field :manage_project_access_tokens,
GraphQL::Types::Boolean,
null: true,
diff --git a/ee/app/models/members/member_role.rb b/ee/app/models/members/member_role.rb
index 2c4819f31dff9e35fb153d73ff60b1e1926d1b71..d5ee2ddddd479ba516e17e25a3cbdeef60a005f5 100644
--- a/ee/app/models/members/member_role.rb
+++ b/ee/app/models/members/member_role.rb
@@ -24,6 +24,9 @@ class MemberRole < ApplicationRecord # rubocop:disable Gitlab/NamespacedClass
},
manage_project_access_tokens: {
description: 'Allows manage access to the project access tokens'
+ },
+ archive_project: {
+ description: 'Allows to archive projects'
}
}.freeze
ALL_CUSTOMIZABLE_PROJECT_PERMISSIONS = [
@@ -32,7 +35,8 @@ class MemberRole < ApplicationRecord # rubocop:disable Gitlab/NamespacedClass
:read_vulnerability,
:admin_merge_request,
:admin_vulnerability,
- :manage_project_access_tokens
+ :manage_project_access_tokens,
+ :archive_project
].freeze
ALL_CUSTOMIZABLE_GROUP_PERMISSIONS = [
:read_dependency,
diff --git a/ee/app/policies/ee/project_policy.rb b/ee/app/policies/ee/project_policy.rb
index c84f722fcd0bbdff35ada7c2510d5a8651f16693..63c952b7a6df9a6b0e22ff39bad9657e83171ab2 100644
--- a/ee/app/policies/ee/project_policy.rb
+++ b/ee/app/policies/ee/project_policy.rb
@@ -637,6 +637,23 @@ module ProjectPolicy
).has_ability?
end
+ condition(:archive_project_enabled) do
+ ::Feature.enabled?(:archive_project, @subject.root_ancestor)
+ end
+
+ desc "Custom role on project that enables archiving projects"
+ condition(:custom_role_enables_archive_projects) do
+ ::Auth::MemberRoleAbilityLoader.new(
+ user: @user,
+ resource: @subject,
+ ability: :archive_project
+ ).has_ability?
+ end
+
+ rule { custom_roles_allowed & archive_project_enabled & custom_role_enables_archive_projects }.policy do
+ enable :archive_project
+ end
+
rule { needs_new_sso_session }.policy do
prevent :read_project
end
diff --git a/ee/config/feature_flags/development/archive_project.yml b/ee/config/feature_flags/development/archive_project.yml
new file mode 100644
index 0000000000000000000000000000000000000000..74b4c6b4a86faf53d93f300d744d9e798c8e16d4
--- /dev/null
+++ b/ee/config/feature_flags/development/archive_project.yml
@@ -0,0 +1,8 @@
+---
+name: archive_project
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134998
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429239
+milestone: "16.6"
+type: development
+group: group::authorization
+default_enabled: false
diff --git a/ee/lib/ee/sidebars/projects/menus/settings_menu.rb b/ee/lib/ee/sidebars/projects/menus/settings_menu.rb
index 5b931a1e0960d12489753c8d1abaf7f95db6d66c..56cc28e7d1faaf72228c85a0f1959761774b9a7c 100644
--- a/ee/lib/ee/sidebars/projects/menus/settings_menu.rb
+++ b/ee/lib/ee/sidebars/projects/menus/settings_menu.rb
@@ -28,6 +28,33 @@ def analytics_menu_item
item_id: :analytics
)
end
+
+ private
+
+ override :enabled_menu_items
+ def enabled_menu_items
+ return super if can?(context.current_user, :admin_project, context.project)
+
+ custom_roles_menu_items
+ end
+
+ def custom_roles_menu_items
+ items = []
+ return items unless context.current_user
+
+ items << general_menu_item if custom_roles_general_menu_item?
+ items << access_tokens_menu_item if custom_roles_access_token_menu_item?
+
+ items
+ end
+
+ def custom_roles_general_menu_item?
+ can?(context.current_user, :archive_project, context.project)
+ end
+
+ def custom_roles_access_token_menu_item?
+ can?(context.current_user, :manage_resource_access_tokens, context.project)
+ end
end
end
end
diff --git a/ee/spec/controllers/projects_controller_spec.rb b/ee/spec/controllers/projects_controller_spec.rb
index 613d1a02c4d0caf18b9a87b7324fdbcaced1816c..21ebd428f55661e8b54dfd5c6ac7228464439b68 100644
--- a/ee/spec/controllers/projects_controller_spec.rb
+++ b/ee/spec/controllers/projects_controller_spec.rb
@@ -170,16 +170,33 @@
end
describe 'GET edit', feature_category: :groups_and_projects do
+ subject(:request) { get :edit, params: { namespace_id: project.namespace.path, id: project.path } }
+
it 'does not allow an auditor user to access the page' do
sign_in(create(:user, :auditor))
- get :edit, params: {
- namespace_id: project.namespace.path,
- id: project.path
- }
+ request
expect(response).to have_gitlab_http_status(:not_found)
end
+
+ context 'when the user can archive projects' do
+ let_it_be(:guest) { create(:user) }
+
+ before do
+ project.add_guest(guest)
+ allow(controller).to receive(:can?).and_call_original
+ allow(controller).to receive(:can?).with(guest, :archive_project, anything).and_return(true)
+
+ sign_in(guest)
+ request
+ end
+
+ it 'allows the user to access the page' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:edit)
+ end
+ end
end
describe 'POST create', feature_category: :groups_and_projects do
diff --git a/ee/spec/features/projects/custom_roles/archive_project_custom_permission_spec.rb b/ee/spec/features/projects/custom_roles/archive_project_custom_permission_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5050be486042025df20c26c2395d2d13bace18d4
--- /dev/null
+++ b/ee/spec/features/projects/custom_roles/archive_project_custom_permission_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Archive Project Custom Permission', feature_category: :groups_and_projects do
+ let_it_be(:project) { create(:project, :in_group) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:custom_role) { create(:member_role, :guest, namespace: project.root_namespace, archive_project: true) }
+
+ before do
+ stub_licensed_features(custom_roles: true)
+ create(:project_member, :guest, member_role: custom_role, user: user, project: project)
+ sign_in(user)
+ end
+
+ context 'when the project is not archived', :js, :aggregate_failures do
+ it 'allows a guest user with custom `archive_project` permissions to archive it' do
+ visit project_path(project)
+
+ within_testid('super-sidebar') do
+ click_button('Settings')
+ click_link('General')
+ end
+
+ click_link('Archive project')
+ click_button('Archive project')
+
+ expect(page).to have_current_path(project_path(project))
+ expect(project.reload.archived?).to eq(true)
+ end
+ end
+
+ context 'when the project is archived', :js, :aggregate_failures do
+ let_it_be(:project) { create(:project, :archived, namespace: project.namespace) }
+
+ it 'allows a guest user with custom `archive_project` permissions to unarchive it' do
+ visit project_path(project)
+
+ within_testid('super-sidebar') do
+ click_button('Settings')
+ click_link('General')
+ end
+
+ click_link('Unarchive project')
+ click_button('Unarchive project')
+
+ expect(page).to have_current_path(project_path(project))
+ expect(project.reload).not_to be_archived
+ end
+ end
+
+ shared_examples 'does not allow the user to archive the project' do
+ it 'does not show the `Settings` sidebar item', :js do
+ visit project_path(project)
+
+ within_testid('super-sidebar') do
+ expect(page).not_to have_button('Settings')
+ end
+ end
+
+ it 'does not allow access to the edit page' do
+ visit edit_project_path(project)
+
+ expect(page).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when the `archive_project` feature flag is disabled' do
+ before do
+ stub_feature_flags(archive_project: false)
+ end
+
+ it_behaves_like 'does not allow the user to archive the project'
+ end
+
+ context 'when the `custom_roles` licensed feature is not available' do
+ before do
+ stub_licensed_features(custom_roles: false)
+ end
+
+ it_behaves_like 'does not allow the user to archive the project'
+ end
+
+ context 'when the user does not have the custom `archive_project` permission' do
+ let_it_be(:custom_role) { create(:member_role, :guest, namespace: project.root_namespace, archive_project: false) }
+
+ it_behaves_like 'does not allow the user to archive the project'
+ end
+end
diff --git a/ee/spec/frontend/roles_and_permissions/components/create_member_role_spec.js b/ee/spec/frontend/roles_and_permissions/components/create_member_role_spec.js
index 2e87d5321c11896d62214bd06574da7cd335c6a1..76600c804636266f4e88841dd5e7b548b91e1153 100644
--- a/ee/spec/frontend/roles_and_permissions/components/create_member_role_spec.js
+++ b/ee/spec/frontend/roles_and_permissions/components/create_member_role_spec.js
@@ -95,6 +95,21 @@ describe('CreateMemberRole', () => {
});
});
+ describe('archive_project feature flag is on', () => {
+ beforeEach(() => {
+ window.gon.features = { archiveProject: true };
+ });
+
+ it('renders archive project permission', () => {
+ const permission = { name: 'Permission E', description: 'Description E' };
+ createComponent({ availablePermissions: [permission] });
+ const checkbox = findCheckboxes().at(0);
+
+ expect(checkbox.text()).toContain(permission.name);
+ expect(checkbox.text()).toContain(permission.description);
+ });
+ });
+
it('emits cancel event', () => {
expect(wrapper.emitted('cancel')).toBeUndefined();
diff --git a/ee/spec/frontend/roles_and_permissions/components/list_member_roles_spec.js b/ee/spec/frontend/roles_and_permissions/components/list_member_roles_spec.js
index 634574006e76f1c70126ec8e519bad6e7a5fed0d..ec49910debd3052ee15a63a6eb28f7f1651bf04c 100644
--- a/ee/spec/frontend/roles_and_permissions/components/list_member_roles_spec.js
+++ b/ee/spec/frontend/roles_and_permissions/components/list_member_roles_spec.js
@@ -96,16 +96,28 @@ describe('ListMemberRoles', () => {
getMemberRoles.mockResolvedValue({ data: mockResponse });
});
- it('shows empty state', () => {
- createComponent({ groupId: null });
- expect(wrapper.findByTestId('card-title').text()).toMatch(ListMemberRoles.i18n.cardTitle);
- expect(findCounter().text()).toBe('0');
- expect(findAddRoleButton().props('disabled')).toBe(true);
- expect(findEmptyState().props()).toMatchObject({
- description: emptyText,
- title: ListMemberRoles.i18n.emptyTitle,
+ describe('empty state', () => {
+ beforeEach(() => {
+ getMemberRoles.mockResolvedValue({ data: [] });
+ createComponent({ groupId });
+ });
+
+ it('shows empty state', () => {
+ expect(wrapper.findByTestId('card-title').text()).toMatch(ListMemberRoles.i18n.cardTitle);
+ expect(findCounter().text()).toBe('0');
+ expect(findAddRoleButton().props('disabled')).toBe(false);
+ expect(findEmptyState().props()).toMatchObject({
+ description: emptyText,
+ title: ListMemberRoles.i18n.emptyTitle,
+ });
+ expect(findCreateMemberRole().exists()).toBe(false);
+ });
+
+ it('hides empty state when toggling the form', async () => {
+ findAddRoleButton().vm.$emit('click');
+ await waitForPromises();
+ expect(findEmptyState().exists()).toBe(false);
});
- expect(findCreateMemberRole().exists()).toBe(false);
});
describe('fetching roles', () => {
diff --git a/ee/spec/lib/ee/api/entities/member_role_spec.rb b/ee/spec/lib/ee/api/entities/member_role_spec.rb
index 9fe99b298de87747977ad46fe4d02ab7c2a15b33..83ee63da11f01a7a85a1ae475e6628a0adfb189f 100644
--- a/ee/spec/lib/ee/api/entities/member_role_spec.rb
+++ b/ee/spec/lib/ee/api/entities/member_role_spec.rb
@@ -20,7 +20,8 @@
expect(subject[:read_code]).to eq member_role.read_code
expect(subject[:read_vulnerability]).to eq member_role.read_vulnerability
expect(subject[:admin_vulnerability]).to eq member_role.admin_vulnerability
- expect(subject[:manage_project_access_tokens]).to eq member_role.admin_vulnerability
+ expect(subject[:manage_project_access_tokens]).to eq member_role.manage_project_access_tokens
+ expect(subject[:archive_project]).to eq member_role.archive_project
expect(subject[:group_id]).to eq(member_role.namespace.id)
end
end
diff --git a/ee/spec/lib/ee/sidebars/projects/menus/settings_menu_spec.rb b/ee/spec/lib/ee/sidebars/projects/menus/settings_menu_spec.rb
index 4f0b754f230bda2ce1d5b542ebf505591b7c3c73..e439bc7ea8290fbeef43775c1efc83094617949d 100644
--- a/ee/spec/lib/ee/sidebars/projects/menus/settings_menu_spec.rb
+++ b/ee/spec/lib/ee/sidebars/projects/menus/settings_menu_spec.rb
@@ -37,5 +37,37 @@
expect(subject).to be_nil
end
end
+
+ describe 'General' do
+ let(:item_id) { :general }
+
+ describe 'when the user is not an admin but has archive_project custom permission' do
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user, :admin_project, project).and_return(false)
+ allow(Ability).to receive(:allowed?).with(user, :archive_project, project).and_return(true)
+ end
+
+ it 'includes general menu item' do
+ expect(subject.title).to eql('General')
+ end
+ end
+ end
+
+ describe 'Access Tokens' do
+ let(:item_id) { :access_tokens }
+
+ describe 'when the user is not an admin but has manage_resource_access_tokens custom permission' do
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user, :admin_project, project).and_return(false)
+ allow(Ability).to receive(:allowed?).with(user, :manage_resource_access_tokens, project).and_return(true)
+ end
+
+ it 'includes access token menu item' do
+ expect(subject.title).to eql('Access Tokens')
+ end
+ end
+ end
end
end
diff --git a/ee/spec/models/ee/user_spec.rb b/ee/spec/models/ee/user_spec.rb
index 74f53c3ec850c272a953e12e8e3099db4e320f08..1d20cb0723539ab8d3ca9b205683c7be5f1c47a6 100644
--- a/ee/spec/models/ee/user_spec.rb
+++ b/ee/spec/models/ee/user_spec.rb
@@ -1182,7 +1182,8 @@
OR admin_vulnerability = true
OR read_dependency = true
OR read_vulnerability = true
- OR manage_project_access_tokens = true\)\)\)\)'.squish # allow_cross_joins_across_databases
+ OR manage_project_access_tokens = true
+ OR archive_project = true\)\)\)\)'.squish # allow_cross_joins_across_databases
end
before do
diff --git a/ee/spec/policies/project_policy_spec.rb b/ee/spec/policies/project_policy_spec.rb
index b2ce51ac62dc2062f1fbf9d7bf0570c28a73e6c8..e360bf39cb80892633c42a30d9ce287050e632e4 100644
--- a/ee/spec/policies/project_policy_spec.rb
+++ b/ee/spec/policies/project_policy_spec.rb
@@ -2761,6 +2761,29 @@ def create_member_role(member, abilities = member_role_abilities)
it { is_expected.to be_disallowed(*disallowed_abilities) }
end
end
+
+ context 'for a member role with archive_project true' do
+ let(:member_role_abilities) { { archive_project: true } }
+ let(:allowed_abilities) { [:archive_project] }
+
+ context 'with archive_project FF enabled' do
+ before do
+ stub_feature_flags(archive_project: [project.group])
+ end
+
+ it_behaves_like 'custom roles abilities'
+ end
+
+ context 'with archive_project FF disabled' do
+ before do
+ stub_feature_flags(archive_project: false)
+ end
+
+ let(:disallowed_abilities) { [:archive_project] }
+
+ it { is_expected.to be_disallowed(*disallowed_abilities) }
+ end
+ end
end
describe 'permissions for suggested reviewers bot', :saas do
diff --git a/ee/spec/requests/api/member_roles_spec.rb b/ee/spec/requests/api/member_roles_spec.rb
index 06ef35355f0b363a02a1924e4b8a6d036eba1a5c..fff5e48f90bfac0e1d8523a24f8124c4fc016751 100644
--- a/ee/spec/requests/api/member_roles_spec.rb
+++ b/ee/spec/requests/api/member_roles_spec.rb
@@ -106,6 +106,7 @@
"admin_merge_request" => false,
"admin_vulnerability" => false,
"manage_project_access_tokens" => false,
+ "archive_project" => false,
"group_id" => group_id
},
{
@@ -120,6 +121,7 @@
"admin_merge_request" => true,
"admin_vulnerability" => false,
"manage_project_access_tokens" => false,
+ "archive_project" => false,
"group_id" => group_id
}
]
diff --git a/ee/spec/views/projects/edit.html.haml_spec.rb b/ee/spec/views/projects/edit.html.haml_spec.rb
index 5c7ec8341d3ac02e4281b38e630d876ef7a8d345..1ff41577815c54005cc41c4487742119473b1a00 100644
--- a/ee/spec/views/projects/edit.html.haml_spec.rb
+++ b/ee/spec/views/projects/edit.html.haml_spec.rb
@@ -38,4 +38,27 @@
it_behaves_like 'does not render registration features prompt', :project_disabled_repository_size_limit
end
end
+
+ context 'when rendering for a user that is not an owner' do
+ let_it_be(:user) { create(:user) }
+
+ before do
+ allow(view).to receive(:can?).with(user, :archive_project, project).and_return(can_archive_projects)
+ render
+ end
+
+ subject { rendered }
+
+ context 'when the user can archive projects' do
+ let(:can_archive_projects) { true }
+
+ it { is_expected.to have_link(_('Archive project')) }
+ end
+
+ context 'when the user cannot archive projects' do
+ let(:can_archive_projects) { false }
+
+ it { is_expected.not_to have_link(_('Archive project')) }
+ end
+ end
end
diff --git a/lib/sidebars/projects/menus/settings_menu.rb b/lib/sidebars/projects/menus/settings_menu.rb
index 8fed1c46425eaccbc36a5eff869c06639a023067..077eebf58b9e1d4d265eeed25963c12355df86dd 100644
--- a/lib/sidebars/projects/menus/settings_menu.rb
+++ b/lib/sidebars/projects/menus/settings_menu.rb
@@ -57,10 +57,6 @@ def enabled_menu_items
monitor_menu_item,
usage_quotas_menu_item
]
- elsif context.current_user && can?(context.current_user, :manage_resource_access_tokens, context.project)
- [
- access_tokens_menu_item
- ]
else
[]
end
diff --git a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
index 81ca9670ac6f152a0803f0a45671b9d47c5ba00c..605cec8be5e3003aa36f75fbaa10b031e6c8eb28 100644
--- a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
@@ -59,18 +59,6 @@
let(:item_id) { :access_tokens }
it_behaves_like 'access rights checks'
-
- describe 'when the user is not an admin but has manage_resource_access_tokens' do
- before do
- allow(Ability).to receive(:allowed?).and_call_original
- allow(Ability).to receive(:allowed?).with(user, :admin_project, project).and_return(false)
- allow(Ability).to receive(:allowed?).with(user, :manage_resource_access_tokens, project).and_return(true)
- end
-
- it 'includes access token menu item' do
- expect(subject.title).to eql('Access Tokens')
- end
- end
end
describe 'Repository' do