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

Also available in: Atom PDF