diff --git a/app/contracts/work_packages/base_contract.rb b/app/contracts/work_packages/base_contract.rb index 2291699355..61d8ade786 100644 --- a/app/contracts/work_packages/base_contract.rb +++ b/app/contracts/work_packages/base_contract.rb @@ -144,7 +144,7 @@ module WorkPackages ret end - def assignable_statuses + def assignable_statuses(include_default = false) # Do not allow skipping statuses without intermediately saving the work package. # We therefore take the original status of the work_package, while preserving all # other changes to it (e.g. type, assignee, etc.) @@ -154,7 +154,11 @@ module WorkPackages model.status end - new_statuses_allowed_from(status) + statuses = new_statuses_allowed_from(status) + + statuses = statuses.or(Status.where_default) if include_default + + statuses.order_by_position end def assignable_types @@ -365,7 +369,7 @@ module WorkPackages statuses = statuses.where(is_closed: false) if model.blocked? - statuses.order_by_position + statuses end def closed_version_and_status?(status = model.status) diff --git a/app/services/work_packages/set_attributes_service.rb b/app/services/work_packages/set_attributes_service.rb index 5628a5785c..8037c7a36c 100644 --- a/app/services/work_packages/set_attributes_service.rb +++ b/app/services/work_packages/set_attributes_service.rb @@ -216,6 +216,6 @@ class WorkPackages::SetAttributesService < ::BaseServices::SetAttributes end def assignable_statuses - instantiate_contract(work_package, user).assignable_statuses.or(Status.where_default.order_by_position) + instantiate_contract(work_package, user).assignable_statuses(true) end end diff --git a/modules/bcf/app/controllers/bcf/api/v2_1/project_extensions/api.rb b/modules/bcf/app/controllers/bcf/api/v2_1/project_extensions/api.rb index f33a20811d..eaf62faba2 100644 --- a/modules/bcf/app/controllers/bcf/api/v2_1/project_extensions/api.rb +++ b/modules/bcf/app/controllers/bcf/api/v2_1/project_extensions/api.rb @@ -31,7 +31,6 @@ module Bcf::API::V2_1 module ProjectExtensions class API < ::API::OpenProjectAPI - get :extensions do mapper = Definitions.new(project: @project, user: current_user) Representer.new(mapper) diff --git a/modules/bcf/app/representers/bcf/api/v2_1/project_extensions/definitions.rb b/modules/bcf/app/representers/bcf/api/v2_1/project_extensions/definitions.rb index fb514aef30..bad9fc9ce1 100644 --- a/modules/bcf/app/representers/bcf/api/v2_1/project_extensions/definitions.rb +++ b/modules/bcf/app/representers/bcf/api/v2_1/project_extensions/definitions.rb @@ -37,26 +37,23 @@ module Bcf::API::V2_1 end def topic_type - project.types.pluck(:name) + contract.assignable_types.pluck(:name) end ## # We only return the default status for now # since that can always be set to a new issue def topic_status - Status - .where_default - .pluck(:name) + contract.assignable_statuses(true).pluck(:name) end def priority - OpenProject::Cache.fetch(IssuePriority.all.cache_key, 'names') do - IssuePriority.all.pluck(:name) - end + contract.assignable_priorities.pluck(:name) end def user_id_type if allowed?(:view_members) + # TODO: Move possible_assignees handling into wp base contract project.possible_assignees.pluck(:mail) else [] @@ -101,10 +98,16 @@ module Bcf::API::V2_1 attr_reader :project, :user + def contract + @contract ||= begin + work_package = WorkPackage.new project: project + WorkPackages::CreateContract.new(work_package, user) + end + end + def allowed?(permission) user.allowed_to?(permission, project) end end end end - diff --git a/modules/bcf/app/representers/bcf/api/v2_1/topics/authorization_representer.rb b/modules/bcf/app/representers/bcf/api/v2_1/topics/authorization_representer.rb new file mode 100644 index 0000000000..b153250802 --- /dev/null +++ b/modules/bcf/app/representers/bcf/api/v2_1/topics/authorization_representer.rb @@ -0,0 +1,55 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +module Bcf::API::V2_1 + class Topics::AuthorizationRepresenter < BaseRepresenter + property :topic_actions, + getter: ->(decorator:, **) { + if decorator.manage_bcf_allowed? + %w[update updateRelatedTopics updateFiles createViewpoint] + else + [] + end + } + + property :topic_status, + getter: ->(decorator:, **) { + if decorator.manage_bcf_allowed? + assignable_statuses.pluck(:name) + else + [] + end + } + + def manage_bcf_allowed? + User.current.allowed_to?(:manage_bcf, represented.model.project) + end + end +end diff --git a/modules/bcf/app/representers/bcf/api/v2_1/topics/single_representer.rb b/modules/bcf/app/representers/bcf/api/v2_1/topics/single_representer.rb index 5578f1fade..6cc8e73278 100644 --- a/modules/bcf/app/representers/bcf/api/v2_1/topics/single_representer.rb +++ b/modules/bcf/app/representers/bcf/api/v2_1/topics/single_representer.rb @@ -134,6 +134,13 @@ module Bcf::API::V2_1 property :bim_snippet, skip_render: true + property :authorization, + getter: ->(*) { + contract = WorkPackages::UpdateContract.new(work_package, User.current) + + Topics::AuthorizationRepresenter.new(contract) + } + def datetime_formatter ::API::V3::Utilities::DateTimeFormatter end diff --git a/modules/bcf/spec/representers/bcf/api/v2_1/project_extensions/definitions_spec.rb b/modules/bcf/spec/representers/bcf/api/v2_1/project_extensions/definitions_spec.rb index aea287a06d..0b650b8224 100644 --- a/modules/bcf/spec/representers/bcf/api/v2_1/project_extensions/definitions_spec.rb +++ b/modules/bcf/spec/representers/bcf/api/v2_1/project_extensions/definitions_spec.rb @@ -37,7 +37,7 @@ describe Bcf::API::V2_1::ProjectExtensions::Definitions, 'rendering' do let(:instance) { described_class.new(project: project, user: user) } describe '#topic_type' do - subject { instance.topic_type } + subject { instance.topic_type } it 'returns the project type names' do expect(subject).to eq ['My BCF type'] @@ -45,9 +45,9 @@ describe Bcf::API::V2_1::ProjectExtensions::Definitions, 'rendering' do end describe '#topic_status' do - let!(:default_status) { FactoryBot.create :default_status } - let!(:status) { FactoryBot.create :status } - subject { instance.topic_status } + let!(:default_status) { FactoryBot.create :default_status } + let!(:status) { FactoryBot.create :status } + subject { instance.topic_status } it 'returns default status only' do expect(subject).to eq [default_status.name] @@ -55,8 +55,8 @@ describe Bcf::API::V2_1::ProjectExtensions::Definitions, 'rendering' do end describe '#priority' do - let!(:priority) { FactoryBot.create :default_priority } - subject { instance.priority } + let!(:priority) { FactoryBot.create :default_priority } + subject { instance.priority } it 'returns statuses for the available types' do expect(subject).to eq [priority.name] @@ -69,7 +69,7 @@ describe Bcf::API::V2_1::ProjectExtensions::Definitions, 'rendering' do member_in_project: project, member_with_permissions: [:view_work_packages]) end - subject { instance.user_id_type } + subject { instance.user_id_type } before do allow(user) @@ -95,7 +95,7 @@ describe Bcf::API::V2_1::ProjectExtensions::Definitions, 'rendering' do end describe '#project_actions' do - subject { instance.project_actions } + subject { instance.project_actions } it 'includes nothing if not permitted' do allow(user).to receive(:allowed_to?).and_return false @@ -118,7 +118,7 @@ describe Bcf::API::V2_1::ProjectExtensions::Definitions, 'rendering' do end describe '#topic_actions' do - subject { instance.topic_actions } + subject { instance.topic_actions } it 'includes nothing if not permitted' do allow(user).to receive(:allowed_to?).and_return false diff --git a/modules/bcf/spec/representers/bcf/api/v2_1/topics/single_representer_rendering_spec.rb b/modules/bcf/spec/representers/bcf/api/v2_1/topics/single_representer_rendering_spec.rb index a097ac48cb..f668a9fafd 100644 --- a/modules/bcf/spec/representers/bcf/api/v2_1/topics/single_representer_rendering_spec.rb +++ b/modules/bcf/spec/representers/bcf/api/v2_1/topics/single_representer_rendering_spec.rb @@ -54,10 +54,36 @@ describe Bcf::API::V2_1::Topics::SingleRepresenter, 'rendering' do .and_return(journals) end end + let(:current_user) { FactoryBot.build_stubbed(:user) } let(:issue) { FactoryBot.build_stubbed(:bcf_issue, work_package: work_package) } + let(:manage_bcf_allowed) { true } + let(:statuses) do + [ + FactoryBot.build_stubbed(:status), + FactoryBot.build_stubbed(:status) + ] + end let(:instance) { described_class.new(issue) } + before do + login_as(current_user) + + allow(current_user) + .to receive(:allowed_to?) + .with(:manage_bcf, issue.project) + .and_return(manage_bcf_allowed) + + contract = double('contract', + model: issue, + assignable_statuses: statuses) + + allow(WorkPackages::UpdateContract) + .to receive(:new) + .with(work_package, current_user) + .and_return(contract) + end + subject { instance.to_json } describe 'attributes' do @@ -173,4 +199,36 @@ describe Bcf::API::V2_1::Topics::SingleRepresenter, 'rendering' do end end end + + describe 'authorization' do + context 'if the user has manage_bcf permission' do + it 'lists the actions' do + expect(subject) + .to be_json_eql(%w[update updateRelatedTopics updateFiles createViewpoint].to_json) + .at_path('authorization/topic_actions') + end + + it 'lists the allowed statuses' do + expect(subject) + .to be_json_eql(statuses.map(&:name).to_json) + .at_path('authorization/topic_status') + end + end + + context 'if the user lacks manage_bcf permission' do + let(:manage_bcf_allowed) { false } + + it 'signals lack of available actions' do + expect(subject) + .to be_json_eql([]) + .at_path('authorization/topic_actions') + end + + it 'lists no allowed status' do + expect(subject) + .to be_json_eql([].to_json) + .at_path('authorization/topic_status') + end + end + end end diff --git a/modules/bcf/spec/requests/api/bcf/v2_1/project_extensions_api_spec.rb b/modules/bcf/spec/requests/api/bcf/v2_1/project_extensions_api_spec.rb index 263aa9e00e..d37e13b1ef 100644 --- a/modules/bcf/spec/requests/api/bcf/v2_1/project_extensions_api_spec.rb +++ b/modules/bcf/spec/requests/api/bcf/v2_1/project_extensions_api_spec.rb @@ -78,11 +78,11 @@ describe 'BCF 2.1 project extensions resource', type: :request, content_type: :j member_with_permissions: [:view_project, :edit_project, :manage_bcf, :view_members]) end - let(:other_user) { + let(:other_user) do FactoryBot.create(:user, member_in_project: project, member_with_permissions: [:view_project]) - } + end before do other_user diff --git a/modules/bcf/spec/requests/api/bcf/v2_1/topics_api_spec.rb b/modules/bcf/spec/requests/api/bcf/v2_1/topics_api_spec.rb index 68af3ac069..eb937d3db4 100644 --- a/modules/bcf/spec/requests/api/bcf/v2_1/topics_api_spec.rb +++ b/modules/bcf/spec/requests/api/bcf/v2_1/topics_api_spec.rb @@ -79,6 +79,19 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma due_date: Date.today, project: project) end + let(:other_status) do + FactoryBot.create(:status).tap do |s| + member = current_user.members.detect { |m| m.project_id == work_package.project_id } + + if member + FactoryBot.create(:workflow, + old_status: work_package.status, + new_status: s, + type: work_package.type, + role: member.roles.first) + end + end + end let(:bcf_issue) { FactoryBot.create(:bcf_issue, work_package: work_package) } subject(:response) { last_response } @@ -90,6 +103,7 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma before do login_as(current_user) bcf_issue + other_status get path end @@ -114,7 +128,11 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma "stage": bcf_issue.stage, "title": work_package.subject, "topic_status": work_package.status.name, - "topic_type": work_package.type.name + "topic_type": work_package.type.name, + "authorization": { + "topic_status": [], + "topic_actions": [] + } } ] end @@ -131,6 +149,41 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma it_behaves_like 'bcf api not allowed response' end + + context 'having edit permission' do + let(:current_user) { edit_member_user } + + it_behaves_like 'bcf api successful response' do + let(:expected_body) do + [ + { + "assigned_to": assignee.mail, + "creation_author": work_package.author.mail, + "creation_date": work_package.created_at.iso8601, + "description": work_package.description, + "due_date": work_package.due_date.iso8601, + "guid": bcf_issue.uuid, + "index": bcf_issue.index, + "labels": bcf_issue.labels, + "priority": work_package.priority.name, + "modified_author": current_user.mail, + "modified_date": work_package.updated_at.iso8601, + "reference_links": [ + api_v3_paths.work_package(work_package.id) + ], + "stage": bcf_issue.stage, + "title": work_package.subject, + "topic_status": work_package.status.name, + "topic_type": work_package.type.name, + "authorization": { + "topic_status": [work_package.status.name, other_status.name], + "topic_actions": %w[update updateRelatedTopics updateFiles createViewpoint] + } + } + ] + end + end + end end describe 'GET /api/bcf/2.1/projects/:project_id/topics/:uuid' do @@ -140,6 +193,7 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma before do login_as(current_user) bcf_issue + other_status get path end @@ -163,7 +217,11 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma "stage": bcf_issue.stage, "title": work_package.subject, "topic_status": work_package.status.name, - "topic_type": work_package.type.name + "topic_type": work_package.type.name, + "authorization": { + "topic_status": [], + "topic_actions": [] + } } end end @@ -185,6 +243,39 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma it_behaves_like 'bcf api not allowed response' end + + context 'having edit permission' do + let(:current_user) { edit_member_user } + + it_behaves_like 'bcf api successful response' do + let(:expected_body) do + { + "assigned_to": assignee.mail, + "creation_author": work_package.author.mail, + "creation_date": work_package.created_at.iso8601, + "description": work_package.description, + "due_date": work_package.due_date.iso8601, + "guid": bcf_issue.uuid, + "index": bcf_issue.index, + "labels": bcf_issue.labels, + "priority": work_package.priority.name, + "modified_author": current_user.mail, + "modified_date": work_package.updated_at.iso8601, + "reference_links": [ + api_v3_paths.work_package(work_package.id) + ], + "stage": bcf_issue.stage, + "title": work_package.subject, + "topic_status": work_package.status.name, + "topic_type": work_package.type.name, + "authorization": { + "topic_status": [work_package.status.name, other_status.name], + "topic_actions": %w[update updateRelatedTopics updateFiles createViewpoint] + } + } + end + end + end end describe 'DELETE /api/bcf/2.1/projects/:project_id/topics/:uuid' do @@ -393,6 +484,19 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma let!(:default_status) do FactoryBot.create(:default_status) end + let(:other_status) do + FactoryBot.create(:status).tap do |s| + member = current_user.members.detect { |m| m.project_id == project.id } + + if member + FactoryBot.create(:workflow, + old_status: status, + new_status: s, + type: type, + role: member.roles.first) + end + end + end let!(:default_type) do FactoryBot.create(:type, is_default: true) end @@ -426,6 +530,7 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma before do login_as(current_user) + other_status post path, params.to_json end @@ -453,7 +558,11 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma creation_date: work_package&.created_at&.iso8601, modified_author: edit_member_user.mail, modified_date: work_package&.updated_at&.iso8601, - description: description + description: description, + "authorization": { + "topic_status": [other_status.name, status.name], + "topic_actions": %w[update updateRelatedTopics updateFiles createViewpoint] + } } end end @@ -489,7 +598,11 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma creation_date: work_package&.created_at&.iso8601, modified_author: edit_member_user.mail, modified_date: work_package&.updated_at&.iso8601, - description: nil + description: nil, + "authorization": { + "topic_status": [default_status.name], + "topic_actions": %w[update updateRelatedTopics updateFiles createViewpoint] + } } end end @@ -515,6 +628,19 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma let(:status) do FactoryBot.create(:status) end + let(:other_status) do + FactoryBot.create(:status).tap do |s| + member = current_user.members.detect { |m| m.project_id == project.id } + + if member + FactoryBot.create(:workflow, + old_status: status, + new_status: s, + type: type, + role: member.roles.first) + end + end + end let!(:default_status) do FactoryBot.create(:default_status) end @@ -546,6 +672,7 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma before do login_as(current_user) + other_status put path, params.to_json end @@ -572,7 +699,11 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma creation_date: work_package&.created_at&.iso8601, modified_author: edit_member_user.mail, modified_date: work_package&.updated_at&.iso8601, - description: description + description: description, + "authorization": { + "topic_status": [other_status.name, status.name], + "topic_actions": %w[update updateRelatedTopics updateFiles createViewpoint] + } } end end @@ -607,7 +738,11 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma creation_date: work_package&.created_at&.iso8601, modified_author: edit_member_user.mail, modified_date: reloaded_work_package&.updated_at&.iso8601, - description: nil + description: nil, + "authorization": { + "topic_status": [default_status.name], + "topic_actions": %w[update updateRelatedTopics updateFiles createViewpoint] + } } end end