diff --git a/app/assets/javascripts/pages/projects/shared/permissions/index.js b/app/assets/javascripts/pages/projects/shared/permissions/index.js
index 4b4589dc46c0e720d47fe2cd1e165da5701274ab..78dd456aad93b9d0c3686dbb36380f435fc8473d 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/index.js
+++ b/app/assets/javascripts/pages/projects/shared/permissions/index.js
@@ -14,6 +14,9 @@ export default function initProjectPermissionsSettings() {
const mountPoint = document.querySelector('.js-project-permissions-form');
const componentPropsEl = document.querySelector('.js-project-permissions-form-data');
+
+ if (!mountPoint) return null;
+
const componentProps = JSON.parse(componentPropsEl.innerHTML);
const {
diff --git a/app/assets/javascripts/pages/shared/mount_badge_settings.js b/app/assets/javascripts/pages/shared/mount_badge_settings.js
index aeb9f2fb8d371e2d62fc6c092e1a431ee158b1d1..9c92d4f8f1e70923a8806bd62841b0e4ccc3fc3b 100644
--- a/app/assets/javascripts/pages/shared/mount_badge_settings.js
+++ b/app/assets/javascripts/pages/shared/mount_badge_settings.js
@@ -5,6 +5,8 @@ import store from '~/badges/store';
export default (kind) => {
const badgeSettingsElement = document.getElementById('badge-settings');
+ if (!badgeSettingsElement) return null;
+
store.dispatch('loadBadges', {
kind,
apiEndpointUrl: badgeSettingsElement.dataset.apiEndpointUrl,
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 1152bdcf0585ec9ed331a7131f25c75168278404..b997b2e0b6b3989316b367e3b9fcccbcc9f5f404 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -29,7 +29,7 @@ class ProjectsController < Projects::ApplicationController
before_action :authorize_read_code!, only: [:refs]
# Authorize
- before_action :authorize_admin_project_or_custom_permissions!, only: :edit
+ before_action :authorize_view_edit_page!, 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]
@@ -612,11 +612,6 @@ 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/policies/project_policy.rb b/app/policies/project_policy.rb
index 255538c538afc05957edc9a2e36b921c8563a5f0..a26758974d6150f74d83af967441560fe62987b1 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -914,6 +914,7 @@ class ProjectPolicy < BasePolicy
rule { can?(:admin_project) }.policy do
enable :read_usage_quotas
+ enable :view_edit_page
end
rule { can?(:project_bot_access) }.policy do
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 9269369c83ebbcace433ba3d930816aa91d73d65..90837a1a2918f5a618dce6059ff181c58563496a 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -118,8 +118,11 @@
= render 'remove_fork', project: @project
= render 'remove', project: @project
-- elsif can?(current_user, :archive_project, @project)
- = render_if_exists 'projects/settings/archive'
+- else
+ - if can?(current_user, :archive_project, @project)
+ = render_if_exists 'projects/settings/archive'
+ - if can?(current_user, :remove_project, @project)
+ = render 'remove', project: @project
.save-project-loader.hide
.center
diff --git a/db/migrate/20231213170159_add_remove_project_to_member_roles.rb b/db/migrate/20231213170159_add_remove_project_to_member_roles.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d13777c1df1f85dfa05bd9cc21f9a01b937bbf33
--- /dev/null
+++ b/db/migrate/20231213170159_add_remove_project_to_member_roles.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class AddRemoveProjectToMemberRoles < Gitlab::Database::Migration[2.2]
+ milestone '16.8'
+ enable_lock_retries!
+
+ def change
+ add_column :member_roles, :remove_project, :boolean, default: false, null: false
+ end
+end
diff --git a/db/schema_migrations/20231213170159 b/db/schema_migrations/20231213170159
new file mode 100644
index 0000000000000000000000000000000000000000..ff5e6e29a3c7971161979cb4054854dab9418729
--- /dev/null
+++ b/db/schema_migrations/20231213170159
@@ -0,0 +1 @@
+f73fde4e3e54fa88d8dba9ec3a98b7dfb8332aaf7a76de73baf899292ed751b1
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 85854dc30178ec1da23671adaae83cb546235454..a6a18a6d35db2da1382109ac36cdcdceb1297904 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -18657,6 +18657,7 @@ CREATE TABLE member_roles (
admin_group_member boolean DEFAULT false NOT NULL,
manage_project_access_tokens boolean DEFAULT false NOT NULL,
archive_project boolean DEFAULT false NOT NULL,
+ remove_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 fbba2f2474b097c2780b5ad4df7ef85edad5d985..2ca522715fbba1e45d08f0544e07fefb44a5ae20 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -5360,6 +5360,7 @@ Input type: `MemberRoleCreateInput`
| `readCode` | [`Boolean`](#boolean) | Permission to read code. |
| `readDependency` | [`Boolean`](#boolean) | Permission to read dependency. |
| `readVulnerability` | [`Boolean`](#boolean) | Permission to read vulnerability. |
+| `removeProject` | [`Boolean`](#boolean) | Permission to delete projects. |
#### Fields
@@ -21293,6 +21294,7 @@ Represents a member role.
| `readCode` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Permission to read code. |
| `readDependency` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Permission to read dependency. |
| `readVulnerability` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Permission to read vulnerability. |
+| `removeProject` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 16.8. This feature is an Experiment. It can be changed or removed at any time. Permission to delete projects. |
### `MergeAccessLevel`
@@ -30610,6 +30612,7 @@ Member role permission.
| `READ_CODE` | Allows read-only access to the source code. |
| `READ_DEPENDENCY` | Allows read-only access to the dependencies. |
| `READ_VULNERABILITY` | Allows read-only access to the vulnerability reports. |
+| `REMOVE_PROJECT` | Allows deletion of projects. |
### `MemberSort`
diff --git a/doc/api/member_roles.md b/doc/api/member_roles.md
index 24ac70990047d5ec9ccd55ba322d20172fcb2912..a482195b94e39f5cf9f6ce7099ca8115dbbafb12 100644
--- a/doc/api/member_roles.md
+++ b/doc/api/member_roles.md
@@ -17,6 +17,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [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.7.
+> - [Delete project introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/139696) in GitLab 16.8.
FLAG:
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` and `manage_project_access_tokens`.
@@ -51,6 +52,7 @@ If successful, returns [`200`](rest/index.md#status-codes) and the following res
| `[].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. |
+| `[].remove_project` | boolean | Permission to delete projects. |
Example request:
@@ -74,7 +76,8 @@ Example response:
"read_dependency": false,
"read_vulnerability": false,
"manage_project_access_tokens": false,
- "archive_project": false
+ "archive_project": false,
+ "remove_project": false
},
{
"id": 3,
@@ -88,7 +91,8 @@ Example response:
"read_dependency": true,
"read_vulnerability": true,
"manage_project_access_tokens": false,
- "archive_project": false
+ "archive_project": false,
+ "remove_project": false
}
]
```
diff --git a/doc/user/custom_roles.md b/doc/user/custom_roles.md
index 1f3628efa391055ed671d9e6ba4b52ea7f85b8af..ac04e7cca0f1c6f1d18b174055931fd688ba8fe2 100644
--- a/doc/user/custom_roles.md
+++ b/doc/user/custom_roles.md
@@ -17,6 +17,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - 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.7.
> - Ability to use the UI to add a user to your group with a custom role, change a user's custom role, or remove a custom role from a group member [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393239) in GitLab 16.7.
+> - Ability to delete projects [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/425959) in GitLab 16.8.
Custom roles allow group Owners or instance administrators to create roles
specific to the needs of their organization.
@@ -114,7 +115,8 @@ 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), revoke merge request approval, 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/migrate_projects.md#archive-a-project). |
+| `archive_project` | GitLab 16.7 and later | Not applicable | [Archive and unarchive projects](project/settings/migrate_projects.md#archive-a-project). |
+| `remove_project` | GitLab 16.8 and later | Not applicable | [Delete projects](project/working_with_projects.md#delete-a-project). |
## Billing and seat usage
diff --git a/ee/app/controllers/ee/projects_controller.rb b/ee/app/controllers/ee/projects_controller.rb
index ae9f40931233bef66bfa95ef542ae35fbfbb09ba..a7d0d6f43951b1f0cbe2bf1612e5f9ee192ce38c 100644
--- a/ee/app/controllers/ee/projects_controller.rb
+++ b/ee/app/controllers/ee/projects_controller.rb
@@ -220,9 +220,5 @@ 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/graphql/mutations/member_roles/create.rb b/ee/app/graphql/mutations/member_roles/create.rb
index 455ae3faac8eb09e451b81052721a36808d518d5..8e2a298c10e5b6b363dcfab98c18663a37499340 100644
--- a/ee/app/graphql/mutations/member_roles/create.rb
+++ b/ee/app/graphql/mutations/member_roles/create.rb
@@ -50,6 +50,10 @@ class Create < Base
GraphQL::Types::Boolean,
required: false,
description: 'Permission to read vulnerability.'
+ argument :remove_project,
+ GraphQL::Types::Boolean,
+ required: false,
+ description: 'Permission to delete projects.'
def resolve(args)
group = authorized_find!(group_path: args.delete(:group_path))
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 1128c0b6b8f65c4fa11f44c9a7845e59052c872d..58ea61772875e6cc480ea6a73c664247f1523ec7 100644
--- a/ee/app/graphql/types/member_roles/member_role_type.rb
+++ b/ee/app/graphql/types/member_roles/member_role_type.rb
@@ -47,6 +47,12 @@ class MemberRoleType < BaseObject
alpha: { milestone: '16.6' },
description: 'Permission to archive projects.'
+ field :remove_project,
+ GraphQL::Types::Boolean,
+ null: true,
+ alpha: { milestone: '16.8' },
+ description: 'Permission to delete 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 2d1726b12f90c839aaf3aee8dfb117583417b87e..8d3cdee5fccd340b9c76ac5fd91ee2d535933bcf 100644
--- a/ee/app/models/members/member_role.rb
+++ b/ee/app/models/members/member_role.rb
@@ -119,8 +119,8 @@ def validate_requirements
requirement = params[:requirement]
next unless self[permission] # skipping permissions not set for the object
- next unless requirement # skipping permissions that have no requirement
- next if self[requirement] # the requierement is met
+ next unless requirement.present? # skipping permissions that have no requirement
+ next if self[requirement] # the requirement is met
errors.add(permission,
format(s_("MemberRole|%{requirement} has to be enabled in order to enable %{permission}."),
diff --git a/ee/app/policies/ee/project_policy.rb b/ee/app/policies/ee/project_policy.rb
index 4a92bb63f5455531c2c1e904caa4c5f1b5228107..3fd800092d809346331365e22204800baefbb10f 100644
--- a/ee/app/policies/ee/project_policy.rb
+++ b/ee/app/policies/ee/project_policy.rb
@@ -655,6 +655,23 @@ module ProjectPolicy
enable :archive_project
end
+ desc "Custom role on project that enables deleting projects"
+ condition(:custom_role_enables_remove_projects) do
+ ::Auth::MemberRoleAbilityLoader.new(
+ user: @user,
+ resource: @subject,
+ ability: :remove_project
+ ).has_ability?
+ end
+
+ rule { custom_roles_allowed & custom_role_enables_remove_projects }.policy do
+ enable :remove_project
+ end
+
+ rule { can?(:admin_project) | can?(:archive_project) | can?(:remove_project) }.policy do
+ enable :view_edit_page
+ end
+
rule { needs_new_sso_session }.policy do
prevent :read_project
end
diff --git a/ee/config/custom_abilities/remove_project.yml b/ee/config/custom_abilities/remove_project.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7ed3a5095a69af3605e16c31a079d6f7102604c6
--- /dev/null
+++ b/ee/config/custom_abilities/remove_project.yml
@@ -0,0 +1,10 @@
+---
+name: remove_project
+description: Allows deletion of projects.
+introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/425959
+introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/139696
+feature_category: groups_and_projects
+milestone: '16.8'
+group_ability: false
+project_ability: true
+requirement: ''
diff --git a/ee/lib/ee/sidebars/projects/menus/settings_menu.rb b/ee/lib/ee/sidebars/projects/menus/settings_menu.rb
index 56cc28e7d1faaf72228c85a0f1959761774b9a7c..1e274561219dd1d01e09af56eeab376f4aca86b9 100644
--- a/ee/lib/ee/sidebars/projects/menus/settings_menu.rb
+++ b/ee/lib/ee/sidebars/projects/menus/settings_menu.rb
@@ -49,7 +49,7 @@ def custom_roles_menu_items
end
def custom_roles_general_menu_item?
- can?(context.current_user, :archive_project, context.project)
+ can?(context.current_user, :view_edit_page, context.project)
end
def custom_roles_access_token_menu_item?
diff --git a/ee/spec/controllers/projects_controller_spec.rb b/ee/spec/controllers/projects_controller_spec.rb
index 463380dcfa5b8c3f55a5e5da5998257ca719e352..0670db7c7dbdd0e30bb56ea74410276e149d4b2f 100644
--- a/ee/spec/controllers/projects_controller_spec.rb
+++ b/ee/spec/controllers/projects_controller_spec.rb
@@ -169,36 +169,6 @@
end
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))
-
- 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
let!(:params) 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
deleted file mode 100644
index 7285adbf042e3f192429a791b1528eda1e3845ae..0000000000000000000000000000000000000000
--- a/ee/spec/features/projects/custom_roles/archive_project_custom_permission_spec.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-# 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 `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/lib/ee/api/entities/member_role_spec.rb b/ee/spec/lib/ee/api/entities/member_role_spec.rb
index 83ee63da11f01a7a85a1ae475e6628a0adfb189f..6b1fe24b342650e7d168b29683135337870511e9 100644
--- a/ee/spec/lib/ee/api/entities/member_role_spec.rb
+++ b/ee/spec/lib/ee/api/entities/member_role_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe EE::API::Entities::MemberRole do
+RSpec.describe EE::API::Entities::MemberRole, feature_category: :permissions do
describe 'exposes expected fields' do
let_it_be(:group) { create(:group) }
let_it_be(:owner) { create(:group_member, :owner, source: group) }
@@ -22,6 +22,7 @@
expect(subject[:admin_vulnerability]).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[:remove_project]).to eq member_role.remove_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 e439bc7ea8290fbeef43775c1efc83094617949d..6c88d80391d20ba21aec5b0fabfdbe56019b2cea 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
@@ -41,15 +41,29 @@
describe 'General' do
let(:item_id) { :general }
- describe 'when the user is not an admin but has archive_project custom permission' do
+ describe 'when the user is not an admin' do
+ let_it_be(:user) { create(:user) }
+
+ before_all do
+ project.add_guest(user)
+ end
+
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')
+ it 'does not include the general menu item' do
+ expect(subject).to be_nil
+ end
+
+ context 'when the user has the `view_edit_page` ability' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :view_edit_page, project).and_return(true)
+ end
+
+ it 'includes the general menu item' do
+ expect(subject.title).to eql('General')
+ end
end
end
end
diff --git a/ee/spec/models/ee/user_spec.rb b/ee/spec/models/ee/user_spec.rb
index 54e8c85d713fcb4c20822f794da273e48e5fb91c..ddea5fa7d659426359559fa9798a3f8dc928df93 100644
--- a/ee/spec/models/ee/user_spec.rb
+++ b/ee/spec/models/ee/user_spec.rb
@@ -1226,7 +1226,8 @@
OR archive_project = true
OR manage_project_access_tokens = true
OR read_dependency = true
- OR read_vulnerability = true\)\)\)\)'.squish # allow_cross_joins_across_databases
+ OR read_vulnerability = true
+ OR remove_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 73f94bddd146850126ba88ddb823167b66c22f01..36a7ce13b413a98b5e5c3f873d21d57744c9e580 100644
--- a/ee/spec/policies/project_policy_spec.rb
+++ b/ee/spec/policies/project_policy_spec.rb
@@ -2780,7 +2780,14 @@ def create_member_role(member, abilities = member_role_abilities)
context 'for a member role with archive_project true' do
let(:member_role_abilities) { { archive_project: true } }
- let(:allowed_abilities) { [:archive_project] }
+ let(:allowed_abilities) { [:archive_project, :view_edit_page] }
+
+ it_behaves_like 'custom roles abilities'
+ end
+
+ context 'for a member role with `remove_project` true' do
+ let(:member_role_abilities) { { remove_project: true } }
+ let(:allowed_abilities) { [:remove_project, :view_edit_page] }
it_behaves_like 'custom roles abilities'
end
diff --git a/ee/spec/requests/api/member_roles_spec.rb b/ee/spec/requests/api/member_roles_spec.rb
index c4b653fa73218b443f9f7505cf3e86385d5baf2a..a1f52f1db2469e76f3d65795b6cc0d4fabf05ff7 100644
--- a/ee/spec/requests/api/member_roles_spec.rb
+++ b/ee/spec/requests/api/member_roles_spec.rb
@@ -107,6 +107,7 @@
"admin_vulnerability" => false,
"manage_project_access_tokens" => false,
"archive_project" => false,
+ "remove_project" => false,
"group_id" => group_id
},
{
@@ -122,6 +123,7 @@
"admin_vulnerability" => false,
"manage_project_access_tokens" => false,
"archive_project" => false,
+ "remove_project" => false,
"group_id" => group_id
}
]
diff --git a/ee/spec/requests/custom_roles/archive_project/request_spec.rb b/ee/spec/requests/custom_roles/archive_project/request_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5520b9159b9f371f39eca192be12298f2621c126
--- /dev/null
+++ b/ee/spec/requests/custom_roles/archive_project/request_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'User with archive_project custom role', feature_category: :permissions do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :in_group) }
+
+ before do
+ stub_licensed_features(custom_roles: true)
+
+ sign_in(user)
+ end
+
+ describe ProjectsController do
+ let_it_be(:role) { create(:member_role, :guest, namespace: project.group, archive_project: true) }
+ let_it_be(:member) { create(:project_member, :guest, member_role: role, user: user, project: project) }
+
+ describe "#edit" do
+ it 'user has access via a custom role' do
+ get edit_project_path(project)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:edit)
+ end
+ end
+
+ describe "#archive" do
+ it 'user has access via a custom role' do
+ post archive_project_path(project)
+
+ expect(project.reload).to be_archived
+ expect(response).to have_gitlab_http_status(:redirect)
+ end
+ end
+
+ describe "#unarchive" do
+ it 'user has access via a custom role' do
+ post unarchive_project_path(project)
+
+ expect(project.reload).not_to be_archived
+ expect(response).to have_gitlab_http_status(:redirect)
+ end
+ end
+ end
+end
diff --git a/ee/spec/requests/custom_roles/delete_project/request_spec.rb b/ee/spec/requests/custom_roles/delete_project/request_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8b42e8fbadbf6ef193e6323064d4960062b1917c
--- /dev/null
+++ b/ee/spec/requests/custom_roles/delete_project/request_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'User with remove_project custom role', feature_category: :permissions do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :in_group) }
+
+ before do
+ stub_licensed_features(custom_roles: true)
+
+ sign_in(user)
+ end
+
+ describe ProjectsController do
+ let_it_be(:role) { create(:member_role, :guest, namespace: project.group, remove_project: true) }
+ let_it_be(:member) { create(:project_member, :guest, member_role: role, user: user, project: project) }
+
+ describe "#edit" do
+ it 'user has access via a custom role' do
+ get edit_project_path(project)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:edit)
+ end
+ end
+
+ describe "#destroy" do
+ it 'user has access via a custom role' do
+ delete project_path(project)
+
+ expect(project.reload).to be_pending_delete
+ expect(response).to have_gitlab_http_status(:found)
+ end
+ end
+ end
+end
diff --git a/ee/spec/views/projects/edit.html.haml_spec.rb b/ee/spec/views/projects/edit.html.haml_spec.rb
index 1ff41577815c54005cc41c4487742119473b1a00..752796896647dc856986b29fa9eb87a175d064a8 100644
--- a/ee/spec/views/projects/edit.html.haml_spec.rb
+++ b/ee/spec/views/projects/edit.html.haml_spec.rb
@@ -39,26 +39,33 @@
end
end
- context 'when rendering for a user that is not an owner' do
+ context 'when rendering for a user that is not an owner', feature_category: :permissions do
let_it_be(:user) { create(:user) }
+ let(:can_archive_projects) { false }
+ let(:can_remove_projects) { false }
+
before do
allow(view).to receive(:can?).with(user, :archive_project, project).and_return(can_archive_projects)
+ allow(view).to receive(:can?).with(user, :remove_project, project).and_return(can_remove_projects)
render
end
subject { rendered }
+ it { is_expected.not_to have_link(_('Archive project')) }
+ it { is_expected.not_to have_text(_('Delete this project')) }
+
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 }
+ context 'when the user can remove projects' do
+ let(:can_remove_projects) { true }
- it { is_expected.not_to have_link(_('Archive project')) }
+ it { is_expected.to have_text(_('Delete this project')) }
end
end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index a9a4575d747cdeb5ca4d86f112e98be477e652fc..9f4bf4f6b369c873f8eb21b58a119c081802fd2e 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -472,12 +472,12 @@ def set_access_level(access_level)
end
end
- context 'reading usage quotas' do
+ context 'reading usage quotas and viewing the edit page' do
%w[maintainer owner].each do |role|
context "with #{role}" do
let(:current_user) { send(role) }
- it { is_expected.to be_allowed(:read_usage_quotas) }
+ it { is_expected.to be_allowed(:read_usage_quotas, :view_edit_page) }
end
end
@@ -485,7 +485,7 @@ def set_access_level(access_level)
context "with #{role}" do
let(:current_user) { send(role) }
- it { is_expected.to be_disallowed(:read_usage_quotas) }
+ it { is_expected.to be_disallowed(:read_usage_quotas, :view_edit_page) }
end
end
@@ -493,11 +493,11 @@ def set_access_level(access_level)
let(:current_user) { admin }
context 'when admin mode is enabled', :enable_admin_mode do
- it { expect_allowed(:read_usage_quotas) }
+ it { expect_allowed(:read_usage_quotas, :view_edit_page) }
end
context 'when admin mode is disabled' do
- it { expect_disallowed(:read_usage_quotas) }
+ it { expect_disallowed(:read_usage_quotas, :view_edit_page) }
end
end
end