Defect #44610
openSpent time custom field value is deleted when a time entry is edited by a user who cannot see the field
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¶
- 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.
- Create "Role B" with the permissions
view_time_entries,log_timeandedit_own_time_entries. Role B has no visibility on Field X. - Add "User B" to a project with Role B as their only role.
- As an administrator, create a time entry for User B with Field X =
TEST-VALUE.GET /time_entries/<id>.jsonas administrator returns Field X =TEST-VALUE. - As User B,
GET /time_entries/<id>.jsondoes not list Field X (expected). - 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 - As administrator,
GET /time_entries/<id>.jsonagain:"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