Redmine plugin that adds a new custom field format, "Remote select
(HTTP)", available on issues like any other custom field. The difference
from the native "List" format is that options aren't typed in statically by
the admin — they're fetched via GET from an endpoint configured on the
field itself, which must respond with a JSON array of
{"value": "...", "label": "..."}.
The field needs to follow every permission rule Redmine already has for
custom fields: role-based visibility, tracker/project association,
required-ness, REST API exposure, filtering/grouping in issue lists. The way
to inherit all of that for free is to implement a real custom field format
(Redmine::FieldFormat), not reinvent any of it on the side. That's why the
plugin registers RemoteSelectFieldFormat < Redmine::FieldFormat::List — it only swaps where the options come from;
everything else (permissions, required-ness, "multiple", etc.) is the
standard behavior of the "list" format it inherits from.
- Endpoint response shape: array of
{value, label}.valueis what gets saved on the issue;labelis what shows up in the<select>. Iflabelis missing,valueis used as the label. - Authentication: a configurable HTTP header per field (name + value,
e.g.
Authorization: Bearer xyz). The value is encrypted at rest viaRedmine::Ciphering(the same mechanism Redmine uses for LDAP/repository passwords) — but this only actually encrypts ifdatabase_cipher_keyis set in the Redmine'sconfig/configuration.yml; without that key,Redmine::Cipheringis a no-op and the value sits in plain text insideformat_store. Setting that key is the responsibility of whoever installs Redmine, not this plugin. - Caching: none for now — fetched on every issue form load. If a given remote endpoint starts to hurt (latency, rate limiting), this is the first thing to revisit.
init.rb
lib/redmine_remote_select/
remote_select_field_format.rb # RemoteSelectFieldFormat (the "remote_select" format)
remote_options_fetcher.rb # GET + JSON parsing/validation, short timeouts
custom_field_patch.rb # remote_url / remote_auth_header_name / remote_auth_header_value on CustomField
custom_fields_controller_patch.rb # applies those 3 extra fields on the admin create/update
app/views/custom_fields/formats/
_remote_select.html.erb # extra inputs on the "New custom field" form
config/locales/{en,pt-BR,fr,ru}.yml
- Copy (or symlink) this folder to
<redmine_root>/plugins/redmine_remote_select. - Restart Redmine (no migration needed — the plugin uses
format_store, a column that already exists in core for field formats to store extra attributes without their own migration). - Admin → Custom fields → New custom field → Issues → Format = "Remote select (HTTP)". Fill in the endpoint, optionally the auth header, and associate the field with trackers/projects/roles as usual.
Manually validated end-to-end against two real Redmine instances, by driving
the actual admin UI (curl session, not mocked) on each: created the custom
field through the real admin form, opened an issue form and got a <select>
populated live from an HTTP test endpoint, saved and reopened the issue with
the chosen value preserved and correctly displayed as its label, took the
endpoint down and confirmed the issue form still loads (empty select, no
500).
- Redmine 4.1.1 (Ruby 2.6.6, Rails 5.2.4.2)
- Redmine 6.1.3 (Ruby 3.4.10, Rails 7.2.3.1, Zeitwerk autoloading)
Real bugs found and fixed this way — see git history:
self.label = ...inremote_select_field_format.rbcrashed the whole app on boot on 4.1:labelis a plain instance method onRedmine::FieldFormat::Basethere, not aclass_attribute— nolabel=writer exists. Now guarded withrespond_to?(:label=).Redmine::Plugin.registerrequires the registered id to match the plugins/ directory name exactly. Fixed to:remote_select.Array#filter_map(Ruby ≥ 2.7 only) crashed option parsing on Ruby 2.6. Replaced with.map { ... }.compact.- The admin create form always failed validation on the very first save
(remote_url is only applied to the record after Redmine's own
create/update already ran, so on that first pass it always reads blank —
see the comment on that in
remote_select_field_format.rb). Removed thevalidate_custom_fieldcheck that made this blocking. - The read-only issue view showed the raw stored value (
"b") instead of its label ("Option B") —Base#formatted_valuejust casts the value to a string, which is correct for the native "list" format (value == label there) but wrong here. Fixed by overridingformatted_value. - Zeitwerk (Redmine 6 only), two separate crashes: a plugin
lib/file must be named after the constant it defines —field_format.rbdefiningRemoteSelectFieldFormatbroke this; renamed toremote_select_field_format.rb. Separately,CustomField.include(...)wrapped inRails.application.config.to_preparesilently never took effect at all on Redmine 6 (the methods were missing at request time, confirmed by a live 500) — replaced with reopeningclass CustomFielddirectly at plugin-load time (the same timing Redmine core itself relies on forfield_attributes/store_accessor), with an empty placeholderRedmineRemoteSelect::CustomFieldPatchmodule kept only so Zeitwerk's filename check still finds what it expects from that file.
Not validated: role-based visibility (hiding the field from a role not
in role_ids). Inheriting from Redmine::FieldFormat::List means this is
100% native, unmodified Redmine behavior — nothing in this plugin touches
visibility/role logic — but it wasn't exercised live because admin accounts
bypass that check entirely, and testing it properly needs a dedicated
non-admin test user, which wasn't worth creating on the shared instances
used for this round of testing.
- Redmine version: validated on 4.1.1 and 6.1.3;
requires_redmine version_or_higher: '4.1.0'ininit.rbreflects the lower bound. 5.x wasn't tested directly — it runs on Rails 6.1, which (like 6.x) defaults to Zeitwerk, so the Zeitwerk-related fixes above should already cover it, but that's an inference from the two endpoints actually tested, not a test of 5.x itself. - Form re-render on validation error: if creating a custom field fails
because of another required field (e.g. blank name), the entered
endpoint/header don't come back filled in on the re-rendered form — they
are only applied after the record has actually saved (see the comment in
custom_fields_controller_patch.rb). No data is lost, it just forces re-typing the endpoint in that specific case. - No caching: each issue form load fetches the endpoint fresh. Fine for the validated case; revisit if a real endpoint's latency/rate limits make this hurt.
- SSRF: the endpoint is a free-form URL typed by a Redmine admin (not a regular user), so the risk is equivalent to any other admin-only integration (webhooks, etc.), but whoever rolls this plugin out to clients should know it makes server-side HTTP requests to wherever the admin points it — don't deploy it on a network where untrusted admins could probe internal services.