Skip to content

Commit

Permalink
Merge pull request #14998 from opf/feature/allow-override-of-static-a…
Browse files Browse the repository at this point in the history
…ttributes

Allow administrators to override static attributes on work packages and projects
  • Loading branch information
oliverguenther committed Mar 20, 2024
2 parents a82020a + 9a6d628 commit 5c816b3
Show file tree
Hide file tree
Showing 15 changed files with 340 additions and 70 deletions.
47 changes: 47 additions & 0 deletions app/contracts/concerns/admin_writable_timestamps.rb
@@ -0,0 +1,47 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 the OpenProject GmbH
#
# 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-2013 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 COPYRIGHT and LICENSE files for more details.
#++

module AdminWritableTimestamps
extend ActiveSupport::Concern

class_methods do
def allow_writable_timestamps(attributes = %i[created_at updated_at])
Array(attributes).each do |attr|
attribute attr,
writable: -> { default_attributes_admin_writable? }
end
end
end

private

# Adds an error if user is archived or not an admin.
def default_attributes_admin_writable?
user.admin? && Setting.apiv3_write_readonly_attributes?
end
end
5 changes: 5 additions & 0 deletions app/contracts/projects/create_contract.rb
Expand Up @@ -28,6 +28,11 @@

module Projects
class CreateContract < BaseContract
include AdminWritableTimestamps
# Projects update their updated_at timestamp due to awesome_nested_set
# so allowing writing here would be useless.
allow_writable_timestamps :created_at

private

def validate_user_allowed_to_manage
Expand Down
8 changes: 5 additions & 3 deletions app/contracts/work_packages/create_contract.rb
Expand Up @@ -30,10 +30,12 @@

module WorkPackages
class CreateContract < BaseContract
include AdminWritableTimestamps
allow_writable_timestamps

attribute :author_id,
writable: false do
errors.add :author_id, :invalid if model.author != user
end
writable: -> { default_attributes_admin_writable? }

attribute :status_id,
# Overriding permission from WP base contract to ignore change_work_package_status for creation,
# because we don't require that permission for writable status during WP creation.
Expand Down
1 change: 1 addition & 0 deletions app/models/journal.rb
Expand Up @@ -63,6 +63,7 @@ class Journal < ApplicationRecord
work_package_children_changed_times
work_package_related_changed_times
working_days_changed
default_attribute_written
system_update
].freeze

Expand Down
9 changes: 9 additions & 0 deletions app/services/work_packages/set_attributes_service.rb
Expand Up @@ -66,6 +66,7 @@ def set_calculated_attributes(attributes)
update_project_dependent_attributes
reassign_invalid_status_if_type_changed
set_templated_description
set_cause_for_readonly_attributes
end

def derivable_attribute
Expand Down Expand Up @@ -416,4 +417,12 @@ def parent_due_later_than_start?

(due && !start) || ((due && start) && (due > start))
end

def set_cause_for_readonly_attributes
return unless work_package.changes.keys.intersect?(%w(created_at updated_at author))

work_package.journal_cause = {
"type" => "default_attribute_written"
}
end
end
8 changes: 8 additions & 0 deletions app/views/admin/settings/api_settings/show.html.erb
Expand Up @@ -39,6 +39,14 @@ See COPYRIGHT and LICENSE files for more details.
<p><%= t(:setting_apiv3_max_page_instructions_html) %></p>
</div>
</div>
<div class="form--field">
<%= setting_check_box :apiv3_write_readonly_attributes, container_class: '-slim' %>
<div class="form--field-instructions">
<p><%= t(:setting_apiv3_write_readonly_attributes_instructions_html,
api_documentation_link: static_link_to(:api_docs)
) %></p>
</div>
</div>
</section>
<section class="form--section">
<fieldset class="form--fieldset">
Expand Down
4 changes: 4 additions & 0 deletions config/constants/settings/definition.rb
Expand Up @@ -62,6 +62,10 @@ class Definition
apiv3_max_page_size: {
default: 1000
},
apiv3_write_readonly_attributes: {
description: "Allow overriding readonly attributes (e.g. createdAt, updatedAt, author) during the creation of resources via the REST API",
default: false
},
app_title: {
default: "OpenProject"
},
Expand Down
11 changes: 11 additions & 0 deletions config/locales/en.yml
Expand Up @@ -1646,6 +1646,7 @@ en:
changes_retracted: "The changes were retracted."

caused_changes:
default_attribute_written: "Read-only attributes written"
dates_changed: "Dates changed"
system_update: "OpenProject system update:"

Expand Down Expand Up @@ -2873,6 +2874,16 @@ en:
If CORS is enabled, these are the origins that are allowed to access OpenProject API.
<br/>
Please check the <a href="%{origin_link}" target="_blank">Documentation on the Origin header</a> on how to specify the expected values.
setting_apiv3_write_readonly_attributes: "Write access to read-only attributes"
setting_apiv3_write_readonly_attributes_instructions_html: >
If enabled, the API will allow administrators to write static read-only attributes during creation,
such as createdAt and updatedAt timestamps.
<br/>
<strong>Warning:</strong> This setting has a use-case for e.g., importing data, but allows
administrators to impersonate the creation of items as other users. All creation requests are being
logged however with the true author.
</br>
For more information on attributes and supported resources, please see the %{api_documentation_link}.
setting_apiv3_max_page_size: "Maximum API page size"
setting_apiv3_max_page_instructions_html: >
Set the maximum page size the API will respond with.
Expand Down
2 changes: 1 addition & 1 deletion docs/api/apiv3/components/schemas/project_model.yml
Expand Up @@ -29,7 +29,7 @@ properties:
createdAt:
type: string
format: date-time
description: Time of creation
description: Time of creation. Can be writable by admins with the `apiv3_write_readonly_attributes` setting enabled.
updatedAt:
type: string
format: date-time
Expand Down
4 changes: 2 additions & 2 deletions docs/api/apiv3/components/schemas/work_package_model.yml
Expand Up @@ -104,12 +104,12 @@ properties:
createdAt:
type: string
format: date-time
description: Time of creation
description: Time of creation. Can be writable by admins with the `apiv3_write_readonly_attributes` setting enabled.
readOnly: true
updatedAt:
type: string
format: date-time
description: Time of the most recent change to the work package
description: Time of the most recent change to the work package. Can be writable by admins with the `apiv3_write_readonly_attributes` setting enabled.
readOnly: true
_links:
type: object
Expand Down
11 changes: 8 additions & 3 deletions lib/open_project/journal_formatter/cause.rb
Expand Up @@ -45,11 +45,16 @@ def render(_key, values, options = { html: true })
private

def cause_type_translation(type)
mapped_type = mapped_cause_type(type)
I18n.t("journals.caused_changes.#{mapped_type}", default: mapped_type)
end

def mapped_cause_type(type)
case type
when 'system_update'
I18n.t("journals.caused_changes.system_update")
when /changed_times/, "working_days_changed"
"dates_changed"
else
I18n.t("journals.caused_changes.dates_changed")
type
end
end

Expand Down
45 changes: 45 additions & 0 deletions spec/contracts/projects/create_contract_spec.rb
Expand Up @@ -42,6 +42,9 @@
status_explanation: project_status_explanation)
end
let(:global_permissions) { [:add_project] }
let(:validated_contract) do
contract.tap(&:validate)
end

subject(:contract) { described_class.new(project, current_user) }

Expand All @@ -52,5 +55,47 @@
expect_valid(true)
end
end

describe "writing read-only attributes" do
shared_examples "can not write" do |attribute, value|
it "can not write #{attribute}", :aggregate_failures do
expect(contract.writable_attributes).not_to include(attribute.to_s)

project.send(:"#{attribute}=", value)
expect(validated_contract).not_to be_valid
expect(validated_contract.errors[attribute]).to include "was attempted to be written but is not writable."
end

context "when enabled for admin", with_settings: { apiv3_write_readonly_attributes: true } do
let(:current_user) { build_stubbed(:admin) }

it_behaves_like "can not write", :updated_at, 1.day.ago

it "can write created_at", :aggregate_failures do
expect(contract.writable_attributes).to include('created_at')

project.created_at = 10.days.ago
expect(validated_contract.errors[attribute]).to be_empty
end
end

context "when disabled for admin", with_settings: { apiv3_write_readonly_attributes: false } do
let(:current_user) { build_stubbed(:admin) }

it_behaves_like "can not write", :created_at, 1.day.ago
it_behaves_like "can not write", :updated_at, 1.day.ago
end

context "when enabled for regular user", with_settings: { apiv3_write_readonly_attributes: true } do
it_behaves_like "can not write", :created_at, 1.day.ago
it_behaves_like "can not write", :updated_at, 1.day.ago
end

context "when disabled for regular user", with_settings: { apiv3_write_readonly_attributes: false } do
it_behaves_like "can not write", :created_at, 1.day.ago
it_behaves_like "can not write", :updated_at, 1.day.ago
end
end
end
end
end

0 comments on commit 5c816b3

Please sign in to comment.