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
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_valuesrow 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
stringandlistformats 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>.jsonwith{"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 whensafe_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.