[#3897] Add specific permission archive_project

See https://community.openproject.org/work_packages/3897

This permission is automatically added to project admin role.

A project can be archived by a user if this user has the archive_project
permission on the project and all its descendants.
pull/11853/head
Christophe Bliard 2 years ago
parent fd352c31ac
commit 5cf125a6f9
No known key found for this signature in database
GPG Key ID: 2BC07603210C3FA4
  1. 25
      app/contracts/projects/archive_contract.rb
  2. 3
      app/controllers/projects/archive_controller.rb
  3. 4
      app/helpers/projects_helper.rb
  4. 4
      app/views/projects/_toolbar.html.erb
  5. 6
      config/initializers/permissions.rb
  6. 3
      config/locales/en.yml
  7. 50
      spec/contracts/projects/archive_contract_spec.rb
  8. 2
      spec/contracts/shared/model_contract_shared_context.rb
  9. 43
      spec/helpers/projects_helper_spec.rb

@ -28,13 +28,36 @@
module Projects
class ArchiveContract < ModelContract
include RequiresAdminGuard
include Projects::Archiver
validate :validate_no_foreign_wp_references
validate :validate_has_archive_project_permission
validate :validate_has_archive_project_permission
protected
def validate_has_archive_project_permission
validate_can_archive_project
validate_can_archive_subprojects
end
def validate_can_archive_project
return if user.allowed_to?(:archive_project, model)
errors.add :base, :error_unauthorized
end
def validate_can_archive_subprojects
return if errors.any?
subprojects_with_missing_permission = model.descendants.reject do |subproject|
user.allowed_to?(:archive_project, subproject)
end
if subprojects_with_missing_permission.any?
errors.add :base, :archive_permission_missing_on_subprojects
end
end
def validate_model?
false
end

@ -28,7 +28,8 @@
class Projects::ArchiveController < ApplicationController
before_action :find_project_by_project_id
before_action :require_admin
before_action :authorize, only: [:create]
before_action :require_admin, only: [:destroy]
def create
change_status_action(:archive)

@ -92,7 +92,7 @@ module ProjectsHelper
end
def project_more_menu_archive_item(project)
if User.current.admin? && project.active?
if User.current.allowed_to?(:archive_project, project) && project.active?
[t(:button_archive),
project_archive_path(project, status: params[:status]),
{ data: { confirm: t('project.archive.are_you_sure', name: project.name) },
@ -103,7 +103,7 @@ module ProjectsHelper
end
def project_more_menu_unarchive_item(project)
if User.current.admin? && !project.active? && (project.parent.nil? || project.parent.active?)
if User.current.admin? && project.archived? && (project.parent.nil? || project.parent.active?)
[t(:button_unarchive),
project_archive_path(project, status: params[:status]),
{ method: :delete,

@ -53,7 +53,7 @@ See COPYRIGHT and LICENSE files for more details.
<% end %>
</li>
<% end %>
<% if User.current.admin? %>
<% if User.current.allowed_to?(:archive_project, @project) %>
<li class="toolbar-item hidden-for-mobile">
<%= link_to(project_archive_path(@project, status: '', name: @project.name),
data: { confirm: t('project.archive.are_you_sure', name: @project.name) },
@ -64,6 +64,8 @@ See COPYRIGHT and LICENSE files for more details.
<span class="button--text"><%= t(:button_archive) %></span>
<% end %>
</li>
<% end %>
<% if User.current.admin? %>
<li class="toolbar-item hidden-for-mobile">
<% label = @project.templated ? 'remove_from_templates' : 'make_template' %>
<%= link_to(project_templated_path(@project),

@ -35,6 +35,12 @@ Rails.application.reloader.to_prepare do
global: true,
contract_actions: { projects: %i[create] }
map.permission :archive_project,
{
'projects/archive': %i[create]
},
require: :member
map.permission :create_backup,
{
admin: %i[index],

@ -767,6 +767,8 @@ en:
archived_ancestor: 'The project has an archived ancestor.'
foreign_wps_reference_version: 'Work packages in non descendant projects reference versions of the project or its descendants.'
attributes:
base:
archive_permission_missing_on_subprojects: "You do not have the permissions required to archive all sub-projects. Please contact your project administrator."
types:
in_use_by_work_packages: "still in use by work packages: %{types}"
enabled_modules:
@ -2313,6 +2315,7 @@ en:
permission_add_work_packages: "Add work packages"
permission_add_messages: "Post messages"
permission_add_project: "Create project"
permission_archive_project: "Archive project"
permission_manage_user: "Create and edit users"
permission_manage_placeholder_user: "Create, edit, and delete placeholder users"
permission_add_subprojects: "Create subprojects"

@ -32,8 +32,58 @@ require 'contracts/shared/model_contract_shared_context'
describe Projects::ArchiveContract do
include_context 'ModelContract shared context'
shared_let(:archivist_role) { create(:role, permissions: %i[archive_project]) }
let(:project) { build_stubbed(:project) }
let(:contract) { described_class.new(project, current_user) }
it_behaves_like 'contract is valid for active admins and invalid for regular users'
context 'when user has archive_project permission' do
let(:project) { create(:project) }
let(:current_user) { create(:user, member_in_project: project, member_through_role: archivist_role) }
include_examples 'contract is valid'
end
context 'with subprojects' do
shared_let(:subproject1) { create(:project) }
shared_let(:subproject2) { create(:project) }
shared_let(:project) { create(:project, children: [subproject1, subproject2]) }
shared_let(:current_user) { create(:user, member_in_project: project, member_through_role: archivist_role) }
shared_examples 'with archive_project permission on all/some/none of subprojects' do
context 'when user does not have archive_project permission on any subprojects' do
include_examples 'contract is invalid', base: :archive_permission_missing_on_subprojects
end
context 'when user has archive_project permission on some subprojects but not all' do
before do
create(:member, user: current_user, project: subproject1, roles: [archivist_role])
end
include_examples 'contract is invalid', base: :archive_permission_missing_on_subprojects
end
context 'when user has archive_project permission on all subprojects' do
before do
create(:member, user: current_user, project: subproject1, roles: [archivist_role])
create(:member, user: current_user, project: subproject2, roles: [archivist_role])
end
include_examples 'contract is valid'
end
end
include_examples 'contract is valid for active admins and invalid for regular users'
include_examples 'with archive_project permission on all/some/none of subprojects'
context 'with deep nesting' do
before do
subproject2.update(parent: subproject1)
end
include_examples 'contract is valid for active admins and invalid for regular users'
include_examples 'with archive_project permission on all/some/none of subprojects'
end
end
end

@ -2,7 +2,7 @@ shared_context 'ModelContract shared context' do # rubocop:disable RSpec/Context
def expect_contract_valid
expect(contract.validate)
.to be(true),
"Contract is invalid with the following errors: #{contract.errors.details}"
"Expected contract to be valid. Got invalid contract with the following errors: #{contract.errors.details}"
end
def expect_contract_invalid(errors = {})

@ -110,4 +110,47 @@ describe ProjectsHelper, type: :helper do
.to eql(((('Abcd ' * 5) + "\n") * 10)[0..-2] + '...')
end
end
describe '#project_more_menu_items' do
shared_let(:project) { create(:project) }
shared_let(:current_user) { create(:user) }
subject(:menu) do
items = project_more_menu_items(project)
# each item is a [label, href, **link_to_options]
items.pluck(0)
end
before do
allow(User).to receive(:current).and_return(current_user)
end
context 'when current user is admin' do
before do
current_user.update(admin: true)
end
it { is_expected.to include(t(:button_archive)) }
end
context 'when current user has archive_project permission' do
before do
create(:member, user: current_user, project:, roles: [build(:role, permissions: [:archive_project])])
end
it { is_expected.to include(t(:button_archive)) }
end
context 'when current user does not have archive_project permission' do
it { is_expected.not_to include(t(:button_archive)) }
end
context 'when project is archived' do
before do
project.update(active: false)
end
it { is_expected.not_to include(t(:button_archive)) }
end
end
end

Loading…
Cancel
Save