Project

General

Profile

Actions

Defect #44610

open

Spent time custom field value is deleted when a time entry is edited by a user who cannot see the field

Added by eXe A.D. 1 day ago. Updated 1 day ago.

Status:
New
Priority:
Normal
Assignee:
-
Category:
Time tracking
Target version:
-
Resolution:
Affected version:

Description

Summary

When a Spent time custom field is visible only to specific roles, and a user whose role has no visibility on that field edits a time entry (web form, bulk edit or REST API), the stored value of the hidden field is deleted from the database. The user never sees the field and does not submit any value for it.

Expected: the value of the hidden field is preserved, since the user can neither see nor change it.
Actual: the custom_values row of the hidden field is deleted.

The problem does not occur when the field is visible to all roles, or when the time entry is edited by a user who has visibility on the field.

Environment

  • Redmine 7.0.2.stable
  • Ruby 4.0.7-p0, Rails 8.1.4
  • Database: PostgreSQL
  • Plugins: redirect_to_after_login 0.0.2, redmine_agile 1.7.0, redmine_checklists 4.1.0, redmine_mermaid 1.1.1, redmine_oauth 4.2.3, redmineup_tags 2.2.0 (none of them patches TimeEntry custom field handling as far as we know; see the analysis below, which points to core code)
  • The same behaviour was also observed on Redmine 6.x.
$ ruby bin/about

Environment:
  Redmine version                7.0.2.stable
  Ruby version                   4.0.7-p0 (2026-09-15) [aarch64-linux-musl]
  Rails version                  8.1.4
  Environment                    production
  Database adapter               PostgreSQL
  Mailer queue                   ActiveJob::QueueAdapters::AsyncAdapter
  Mailer delivery                smtp
Redmine settings:
  Redmine theme                  Zitycard (includes JavaScript)
SCM:
  Subversion                     1.14.5
  Mercurial                      7.2.4
  Bazaar                         3.3.15
  Git                            2.54.0
  Filesystem
Redmine plugins:
  redirect_to_after_login        0.0.2
  redmine_agile                  1.7.0
  redmine_checklists             4.1.0
  redmine_mermaid                1.1.1
  redmine_oauth                  4.2.3
  redmineup_tags                 2.2.0

Steps to reproduce

  1. Create a custom field "Field X" of type Spent time, format Text, not required, no default value, with Visible set to "to these roles only" and only "Role A" selected.
  2. Create "Role B" with the permissions view_time_entries, log_time and edit_own_time_entries. Role B has no visibility on Field X.
  3. Add "User B" to a project with Role B as their only role.
  4. As an administrator, create a time entry for User B with Field X = TEST-VALUE. GET /time_entries/<id>.json as administrator returns Field X = TEST-VALUE.
  5. As User B, GET /time_entries/<id>.json does not list Field X (expected).
  6. As User B, edit only the comment of the time entry, either via the web UI (Spent time > Edit > Save) or via the API:
    PUT /time_entries/<id>.json   (User B's API key)
    {"time_entry":{"comments":"changed"}}
    => 204 No Content
    
  7. As administrator, GET /time_entries/<id>.json again:
    "custom_fields":[ ..., {"id":<Field X id>,"name":"Field X","value":null} ]
    

Results

Edit path Changed attribute Edited by Field X after save
REST API (PUT), 3 runs comments User B (no visibility) deleted
Web form comments User B (no visibility) deleted
Web form activity User B (no visibility) deleted
Bulk edit (1 entry) activity User B (no visibility) deleted
REST API (PUT) - control comments Administrator preserved

Database check (read-only) on one of the entries, before and after User B's edit:

SELECT id, custom_field_id, value FROM custom_values
 WHERE customized_type = 'TimeEntry' AND customized_id = <id>;

-- before
 id | custom_field_id | value
  1 | <Field Y id>    | 
  2 | <Field X id>    | TEST-VALUE

-- after User B's edit
 id | custom_field_id | value
  1 | <Field Y id>    | 

The row is deleted, not set to an empty value.

Analysis

In TimeEntry#safe_attributes= (app/models/time_entry.rb), after assigning the attributes:

# Delete assigned custom fields not visible by the user
editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
self.custom_field_values.delete_if do |c|
  !editable_custom_field_ids.include?(c.custom_field.id.to_s)
end

This removes the non-visible fields from the in-memory custom_field_values collection. Then save_custom_field_values (acts_as_customizable) builds the target values only from custom_field_values and assigns them with self.custom_values = target_custom_values. Since has_many :custom_values is declared with :dependent => :delete_all, the stored CustomValue of each non-visible field is deleted.

This block was introduced by #33550 to ignore values submitted for non-visible fields, but it also discards the persisted values.

Possible fix (untested)

Instead of removing the non-editable fields from the collection, restore their original value:

# Restore values of custom fields not editable by the user
editable_ids = editable_custom_field_values(user).map(&:custom_field_id)
custom_field_values.each do |cfv|
  cfv.value = cfv.value_was unless editable_ids.include?(cfv.custom_field_id)
end

This keeps the stored value untouched and still prevents the user from changing it. It might also be relevant to #35132 (default value not applied when the field is not accessible), since for new records value_was should hold the default value.

A regression test could be: a user with visibility sets the value; a user without visibility edits another attribute; the value must still be present.

Related issues


Files

Actions #1

Updated by eXe A.D. 1 day ago

Update: reproduced on a clean installation without plugins

To rule out plugins and local configuration, the issue was reproduced on a fresh instance:

  • Official Docker image redmine:7.0.2-alpine3.24, unmodified
  • Rails 8.1.4, Ruby 4.0.7, PostgreSQL 14.24
  • No plugins, default data (redmine:load_default_data), default configuration, REST API enabled

Same scenario as above (Role A sees the field, Role B does not).

Results

  • 18/18 edits by a user without visibility deleted the custom_values row of the restricted fields (REST API, edit form, bulk edit, context menu). Each request returned success (HTTP 204 / "Successful update") with no warning.
  • 6/6 controls (edited by a user with Role A, or by an administrator) preserved the value.
  • Both string and list formats are affected. A field visible to all roles in the same entry is not affected.

Additional findings

  • Any edit triggers it: changing only hours or activity, saving the form without changes, or an empty PUT /time_entries/<id>.json with {"time_entry":{}}.
  • If the user submits a value for the hidden field, that value is ignored (as expected), but the stored value is still deleted.
  • At model level, a plain save! keeps the value. The loss happens only when safe_attributes= is called as the user without visibility and the entry is then saved, even with empty attributes. This matches the analysis in the description.
  • Inconsistency with issues: an Issue custom field with the same visibility setup keeps its value when the same kind of user edits the issue (API, edit form and bulk edit).

Side observation (possibly a separate issue)

The time entry bulk edit form shows the restricted custom fields to Role B (empty inputs), while the single edit form correctly hides them. The equivalent issue bulk edit form hides them. We have not tested whether a value submitted from that form is saved.

Reproduction script

Attached: timeentry-cf-visibility-repro.zip (disposable Docker Compose setup for Redmine 7.0.2 + PostgreSQL 14, a bootstrap script and a self-contained Python reproduction using only an administrator API key). See the README inside; the script exits with code 10 when the defect is reproduced.

Actions

Also available in: Atom PDF