Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

redmine_remote_select

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": "..."}.

Why a native plugin, not something else

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.

What was decided (from the open questions)

  • Endpoint response shape: array of {value, label}. value is what gets saved on the issue; label is what shows up in the <select>. If label is missing, value is used as the label.
  • Authentication: a configurable HTTP header per field (name + value, e.g. Authorization: Bearer xyz). The value is encrypted at rest via Redmine::Ciphering (the same mechanism Redmine uses for LDAP/repository passwords) — but this only actually encrypts if database_cipher_key is set in the Redmine's config/configuration.yml; without that key, Redmine::Ciphering is a no-op and the value sits in plain text inside format_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.

Structure

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

Installation

  1. Copy (or symlink) this folder to <redmine_root>/plugins/redmine_remote_select.
  2. 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).
  3. 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.

Validated

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 = ... in remote_select_field_format.rb crashed the whole app on boot on 4.1: label is a plain instance method on Redmine::FieldFormat::Base there, not a class_attribute — no label= writer exists. Now guarded with respond_to?(:label=).
  • Redmine::Plugin.register requires 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 the validate_custom_field check that made this blocking.
  • The read-only issue view showed the raw stored value ("b") instead of its label ("Option B") — Base#formatted_value just casts the value to a string, which is correct for the native "list" format (value == label there) but wrong here. Fixed by overriding formatted_value.
  • Zeitwerk (Redmine 6 only), two separate crashes: a plugin lib/ file must be named after the constant it defines — field_format.rb defining RemoteSelectFieldFormat broke this; renamed to remote_select_field_format.rb. Separately, CustomField.include(...) wrapped in Rails.application.config.to_prepare silently never took effect at all on Redmine 6 (the methods were missing at request time, confirmed by a live 500) — replaced with reopening class CustomField directly at plugin-load time (the same timing Redmine core itself relies on for field_attributes/store_accessor), with an empty placeholder RedmineRemoteSelect::CustomFieldPatch module 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.

Known limitations

  • Redmine version: validated on 4.1.1 and 6.1.3; requires_redmine version_or_higher: '4.1.0' in init.rb reflects 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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages