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