CVE-2026-40869
Description
Decidim is a participatory democracy framework. Starting in version 0.19.0 and prior to versions 0.30.5 and 0.31.1, a vulnerability allows any registered and authenticated user to accept or reject any amendments. The impact is on any users who have created proposals where the amendments feature is enabled. This also elevates the user accepting the amendment as the author of the original proposal as people amending proposals are provided coauthorship on the coauthorable resources. Versions 0.30.5 and 0.31.1 fix the issue. As a workaround, disable amendment reactions for the amendable component (e.g. proposals).
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
decidim-coreRubyGems | >= 0.31.0.rc1, < 0.31.1 | 0.31.1 |
decidim-coreRubyGems | >= 0.19.0, < 0.30.5 | 0.30.5 |
Affected products
1Patches
11b99136a1c7aAdmin configuration of amendments by step (#5178)
25 files changed · +504 −222
decidim-admin/app/assets/javascripts/decidim/admin/form.js.es6+20 −8 modified@@ -1,17 +1,29 @@ -// Checks if the form contains a field with a special CSS class added in -// Decidim::Admin::SettingsHelper. If so, prevents the checkbox from being clicked, -// extracts the stored text and adds a new paragraph after the field. +// Checks if the form contains fields with special CSS classes added in +// Decidim::Admin::SettingsHelper and acts accordingly. $(() => { - const $checkbox = $(".participatory_texts_disabled"); + // Prevents checkbox with ".participatory_texts_disabled" class from being clicked. + const $participatoryTexts = $(".participatory_texts_disabled"); - $checkbox.click((event) => { + $participatoryTexts.click((event) => { event.preventDefault(); return false; }); - if ($checkbox.length > 0) { - const $text = $checkbox[0].dataset.text + // (1) Hides fields with ".amendments_step_settings" class if amendments_enabled + // component setting is NOT checked. + // (2) Toggles visibilty of fields with ".amendments_step_settings" class when + // amendments_enabled component setting is clicked. + const $amendmentsEnabled = $("input#component_settings_amendments_enabled"); - $checkbox.parent().after(`<p class="help-text">${$text}</p>`) + if ($amendmentsEnabled.length > 0) { + const $amendmentStepSettings = $(".amendments_step_settings").parent(); + + if ($amendmentsEnabled.is(":not(:checked)")) { + $amendmentStepSettings.hide().siblings(".help-text").hide(); + } + + $amendmentsEnabled.click(() => { + $amendmentStepSettings.toggle().siblings(".help-text").toggle(); + }); } });
decidim-admin/app/assets/stylesheets/decidim/admin/modules/_forms.scss+8 −0 modified@@ -134,3 +134,11 @@ textarea{ cursor: not-allowed; opacity: .5; } + +.edit_component .step-settings{ + .card-section fieldset legend{ + font-size: 24px; + font-weight: bold; + border-bottom: 1px solid grey; + } +}
decidim-admin/app/helpers/decidim/admin/settings_helper.rb+31 −9 modified@@ -30,29 +30,51 @@ def settings_attribute_input(form, attribute, name, options = {}) end end + # Returns a translation or nil. If nil, ZURB Foundation won't add the help_text. + def help_text_for_component_setting(field_name, settings_name, component_name) + key = "decidim.components.#{component_name}.settings.#{settings_name}.#{field_name}_help" + return t(key) if I18n.exists?(key) + end + private def form_method_for_attribute(attribute) return :editor if attribute.type.to_sym == :text && attribute.editor? TYPES[attribute.type.to_sym] end - # Marks :participatory_texts_enabled checkbox with a unique class if - # the Proposals component has existing proposals, and stores the help text - # that will be added in a new div via JavaScript in "decidim/admin/form". - # - # field_name - The name of the field to disable. - # + # Handles special cases. # Returns an empty Hash or a Hash with extra HTML options. def extra_options_for(field_name) - return {} unless field_name == :participatory_texts_enabled && - Decidim::Proposals::Proposal.where(component: @component).any? + case field_name + when :participatory_texts_enabled + participatory_texts_extra_options + when :amendment_creation_enabled, + :amendment_reaction_enabled, + :amendment_promotion_enabled + amendments_extra_options + else + {} + end + end + + # Marks :participatory_texts_enabled setting with a CSS class if the + # Proposals component has existing proposals, so it can be identified + # in "decidim/admin/form.js". Also, adds a help_text. + def participatory_texts_extra_options + return {} unless Decidim::Proposals::Proposal.where(component: @component).any? { class: "participatory_texts_disabled", - data: { text: t("decidim.admin.components.form.participatory_texts_enabled_help") } + help_text: help_text_for_component_setting(:participatory_texts_enabled, :global, :proposals) } end + + # Marks component_step_settings related to amendments with a CSS class, + # so they can be identified in "decidim/admin/form.js". + def amendments_extra_options + { class: "amendments_step_settings" } + end end end end
decidim-admin/app/views/decidim/admin/components/_settings_fields.html.erb+2 −1 modified@@ -4,6 +4,7 @@ attribute, name, label: t("decidim.components.#{component.manifest.name}.settings.#{settings_name}.#{name}"), - tabs_prefix: tabs_prefix + tabs_prefix: tabs_prefix, + help_text: help_text_for_component_setting(name, settings_name, component.manifest.name) ) %> <% end %>
decidim-admin/config/locales/en.yml+0 −1 modified@@ -247,7 +247,6 @@ en: form: default_step_settings: Default step settings global_settings: Global settings - participatory_texts_enabled_help: Cannot interact with this setting if there are existing proposals. Please, create a new `Proposals component` if you want to enable this feature or discard all imported proposals in the `Participatory Texts` menu if you want to disable it. step_settings: Step settings index: add: Add component
decidim-core/app/cells/decidim/amendable/amenders_list/show.erb+1 −1 modified@@ -1,4 +1,4 @@ -<div class="card extra"> +<div class="card extra amender-list"> <div class="definition-data__item"> <span class="definition-data__title"> <%= t("amended_by", scope: "decidim.amendments.amendable") %>
decidim-core/app/cells/decidim/amendable/announcement_cell.rb+20 −8 modified@@ -13,22 +13,34 @@ def show def announcement { - announcement: emendation_message, + announcement: emendation_message + promoted_message, callout_class: state_classes } end def emendation_message - t(model.emendation_state, + message(model.emendation_state, amendable_type, proposal_link, announcement_date) + end + + def promoted_message + return "" unless model.amendment.promoted? + proposal = model.linked_promoted_resource + text = message(:promoted, amendable_type) + %(<br><strong>#{proposal_link(proposal, text)}</strong>) + end + + def message(state, type, link = nil, date = nil) + t(state, scope: "decidim.amendments.emendation.announcement", - amendable_type: amendable_type, - amendable_link: amendable_link, - announcement_date: announcement_date) + amendable_type: type, + proposal_link: link, + date: date) end - def amendable_link - link_to resource_locator(model.amendable).path do - %(<strong>#{present(model.amendable).title}</strong>) + def proposal_link(resource = model.amendable, text = nil) + text ||= %(<strong>#{present(model.amendable).title}</strong>) + link_to resource_locator(resource).path do + text end end
decidim-core/app/commands/decidim/amendable/promote.rb+7 −4 modified@@ -26,6 +26,7 @@ def call transaction do promote_emendation! notify_amendable_and_emendation_authors_and_followers + link_promoted_emendation_and_proposal end broadcast(:ok, @promoted_emendation) @@ -35,10 +36,8 @@ def call attr_reader :form - # The log of this action contains unique information - # that is used to show/hide the promote button - # - # extra_log_info = { promoted_form: emendation.id } + # The log of this action contains unique information: + # extra_log_info = { promoted_from: emendation.id } def promote_emendation! @promoted_emendation = Decidim.traceability.perform_action!( "promote", @@ -72,6 +71,10 @@ def notify_amendable_and_emendation_authors_and_followers def proposal? @amendable.amendable_type == "Decidim::Proposals::Proposal" end + + def link_promoted_emendation_and_proposal + @promoted_emendation.link_resources(@emendation, "created_from_rejected_emendation") + end end end end
decidim-core/app/controllers/concerns/decidim/amendments_controller.rb+4 −5 modified@@ -14,8 +14,7 @@ def new def create @form = form(Decidim::Amendable::CreateForm).from_params(params) - enforce_permission_to :create, :amendment - + enforce_permission_to :create, :amendment, current_component: @form.component Decidim::Amendable::Create.call(@form) do on(:ok) do flash[:notice] = t("created.success", scope: "decidim.amendments") @@ -31,7 +30,7 @@ def create def reject @form = form(Decidim::Amendable::RejectForm).from_params(params) - enforce_permission_to :reject, :amendment, amendment: @form.amendable + enforce_permission_to :reject, :amendment, amendment: @form.amendable, current_component: @form.component Decidim::Amendable::Reject.call(@form) do on(:ok) do @@ -47,7 +46,7 @@ def reject def promote @form = Decidim::Amendable::PromoteForm.from_params(params) - enforce_permission_to :promote, :amendment, amendment: @form.emendation + enforce_permission_to :promote, :amendment, amendment: @form.emendation, current_component: @form.component Decidim::Amendable::Promote.call(@form) do on(:ok) do |proposal| @@ -68,7 +67,7 @@ def review def accept @form = Decidim::Amendable::ReviewForm.from_params(params) - enforce_permission_to :accept, :amendment, amendment: @form.amendable + enforce_permission_to :accept, :amendment, amendment: @form.amendable, current_component: @form.component Decidim::Amendable::Accept.call(@form) do on(:ok) do
decidim-core/app/forms/decidim/amendable/form.rb+4 −0 modified@@ -53,6 +53,10 @@ def run_validations amendable_form.validate end + + def component + @component ||= amendable&.component || emendation&.component + end end end end
decidim-core/app/helpers/decidim/amendments_helper.rb+5 −13 modified@@ -16,7 +16,7 @@ def amendments_for(amendable) content << cell("decidim/collapsible_list", amendable.emendations, cell_options: { context: { current_user: current_user } }, - list_class: "row small-up-1 medium-up-2 card-grid", + list_class: "row small-up-1 medium-up-2 card-grid amendment-list", size: 4).to_s content_tag :div, content.html_safe, class: "section" @@ -41,6 +41,7 @@ def emendation_announcement_for(emendation) # Returns Html action button card to AMEND an amendable resource def amend_button_for(amendable) return unless amendments_enabled? && amendable.amendable? + return unless current_component.current_settings.amendment_creation_enabled cell("decidim/amendable/amend_button_card", amendable) end @@ -76,7 +77,7 @@ def amendments_enabled? def can_react_to_emendation?(emendation) return unless current_user && emendation.emendation? - true + current_component.current_settings.amendment_reaction_enabled end # Checks if the user can accept and reject the emendation @@ -89,18 +90,9 @@ def allowed_to_accept_and_reject?(emendation) # Checks if the user can promote the emendation def allowed_to_promote?(emendation) return unless emendation.amendment.rejected? && emendation.created_by?(current_user) - return if promoted?(emendation) + return if emendation.amendment.promoted? - true - end - - # Checks if the unique ActionLog created in the promote command exists. - def promoted?(emendation) - logs = Decidim::ActionLog.where(decidim_component_id: emendation.component) - .where(decidim_user_id: emendation.creator_author) - .where(action: "promote") - - logs.select { |log| log.extra["promoted_from"] == emendation.id }.present? + current_component.current_settings.amendment_promotion_enabled end # Renders a UserGroup select field in a form.
decidim-core/app/models/decidim/amendment.rb+8 −2 modified@@ -2,11 +2,13 @@ module Decidim class Amendment < ApplicationRecord + STATES = %w(evaluating accepted rejected withdrawn).freeze + belongs_to :amendable, foreign_key: "decidim_amendable_id", foreign_type: "decidim_amendable_type", polymorphic: true belongs_to :amender, foreign_key: "decidim_user_id", class_name: "Decidim::User" belongs_to :emendation, foreign_key: "decidim_emendation_id", foreign_type: "decidim_emendation_type", polymorphic: true - STATES = %w(evaluating accepted rejected withdrawn).freeze + validates :amendable, :amender, :emendation, presence: true def evaluating? state == "evaluating" @@ -16,6 +18,10 @@ def rejected? state == "rejected" end - validates :amendable, :amender, :emendation, presence: true + def promoted? + return false unless rejected? + + emendation.linked_promoted_resource.present? + end end end
decidim-core/app/permissions/decidim/permissions.rb+11 −4 modified@@ -82,10 +82,17 @@ def follow_action? def amend_action? return unless permission_action.subject == :amendment - return allow! if permission_action.action == :create - return allow! if permission_action.action == :reject - return allow! if permission_action.action == :promote - return allow! if permission_action.action == :accept + return disallow! unless component.settings.amendments_enabled + + case permission_action.action + when :create + return allow! if component.current_settings.amendment_creation_enabled + when :accept, + :reject + return allow! if component.current_settings.amendment_reaction_enabled + when :promote + return allow! if component.current_settings.amendment_promotion_enabled + end amendment = context.fetch(:amendment, nil) toggle_allow(amendment&.amender == user)
decidim-core/config/locales/en.yml+6 −5 modified@@ -153,14 +153,15 @@ en: help_text: Review the changes and accept or reject this amendment. A notification will be sent to its author(s). announcement: accepted: |- - This amendment for the %{amendable_type} %{amendable_link} has been - accepted on <strong>%{announcement_date}</strong>. + This amendment for the %{amendable_type} %{proposal_link} has been + accepted on <strong>%{date}</strong>. evaluating: |- - This amendment for the %{amendable_type} %{amendable_link} + This amendment for the %{amendable_type} %{proposal_link} is being evaluated. - rejected: This amendment for the %{amendable_type} %{amendable_link} has been rejected on <strong>%{announcement_date}</strong>. + promoted: Promoted to a %{amendable_type}. + rejected: This amendment for the %{amendable_type} %{proposal_link} was rejected on <strong>%{date}</strong>. withdrawn: |- - This amendment for the %{amendable_type} %{amendable_link} + This amendment for the %{amendable_type} %{proposal_link} has been withdrawn by the author. new: amendment_author: Amendment author
decidim-core/lib/decidim/amendable.rb+8 −1 modified@@ -84,7 +84,7 @@ def resource_state end def emendation_state - return resource_state if resource_state == "withdrawn" + return resource_state if resource_state == "withdrawn" # Special case for Proposals amendment.state end @@ -94,5 +94,12 @@ def state resource_state end + + # Returns the linked resource to or from this model + # for the given resource name and link name. + # See Decidim::Resourceable#link_resources + def linked_promoted_resource + linked_resources(amendable_type, "created_from_rejected_emendation").first + end end end
decidim-proposals/app/cells/decidim/proposals/proposal_linked_resources_cell.rb+14 −0 added@@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require "cell/partial" + +module Decidim + module Proposals + # This cell renders the linked resource of a proposal. + class ProposalLinkedResourcesCell < Decidim::ViewModel + def show + render if linked_resource + end + end + end +end
decidim-proposals/app/cells/decidim/proposals/proposal_linked_resources/show.erb+9 −0 added@@ -0,0 +1,9 @@ +<div class="text-center mt-sm"> + <%= link_to_resource %> + + <div class="text-center"> + <small> + <%= link_help_text %> + </small> + </div> +</div>
decidim-proposals/app/cells/decidim/proposals/proposal_link_to_collaborative_draft_cell.rb+4 −8 modified@@ -5,19 +5,15 @@ module Decidim module Proposals # This cell renders the link to the source collaborative draft of a proposal. - class ProposalLinkToCollaborativeDraftCell < Decidim::ViewModel - def show - render if collaborative_draft - end - + class ProposalLinkToCollaborativeDraftCell < ProposalLinkedResourcesCell private - def collaborative_draft - @collaborative_draft ||= model.linked_resources(:collaborative_draft, "created_from_collaborative_draft").first + def linked_resource + @linked_resource ||= model.linked_resources(:collaborative_draft, "created_from_collaborative_draft").first end def link_to_resource - link_to resource_locator(collaborative_draft).path, class: "link" do + link_to resource_locator(linked_resource).path, class: "link" do t("link_to_collaborative_draft_text", scope: "decidim.proposals.proposals.show") end end
decidim-proposals/app/cells/decidim/proposals/proposal_link_to_rejected_emendation_cell.rb+34 −0 added@@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "cell/partial" + +module Decidim + module Proposals + # This cell renders the link to the rejected emendation promoted to proposal. + class ProposalLinkToRejectedEmendationCell < ProposalLinkedResourcesCell + private + + def linked_resource + @linked_resource ||= model.linked_promoted_resource + end + + def link_to_resource + link_to resource_locator(linked_resource).path, class: "link" do + if model.emendation? + t("link_to_proposal_from_emendation_text", scope: "decidim.proposals.proposals.show") + else + t("link_to_promoted_emendation_text", scope: "decidim.proposals.proposals.show") + end + end + end + + def link_help_text + if model.emendation? + t("link_to_proposal_from_emendation_help_text", scope: "decidim.proposals.proposals.show") + else + t("link_to_promoted_emendation_help_text", scope: "decidim.proposals.proposals.show") + end + end + end + end +end
decidim-proposals/app/cells/decidim/proposals/proposal_link_to_rejected_emendation/show.erb+0 −0 renameddecidim-proposals/app/views/decidim/proposals/proposals/show.html.erb+1 −0 modified@@ -93,6 +93,7 @@ edit_link( <%= render partial: "decidim/shared/share_modal" %> <%= embed_modal_for proposal_proposal_widget_url(@proposal, format: :js) %> <%= cell "decidim/proposals/proposal_link_to_collaborative_draft", @proposal %> + <%= cell "decidim/proposals/proposal_link_to_rejected_emendation", @proposal %> </div> <div class="columns mediumlarge-8 mediumlarge-pull-4"> <div class="section">
decidim-proposals/config/locales/en.yml+12 −0 modified@@ -87,6 +87,7 @@ en: settings: global: amendments_enabled: Amendments enabled + amendments_enabled_help: If active, configure Amendment features for each step. announcement: Announcement attachments_allowed: Allow attachments can_accumulate_supports_beyond_threshold: Can accumulate supports beyond threshold @@ -97,6 +98,7 @@ en: new_proposal_help_text: New proposal help text official_proposals_enabled: Official proposals enabled participatory_texts_enabled: Participatory texts enabled + participatory_texts_enabled_help: Cannot interact with this setting if there are existing proposals. Please, create a new `Proposals component` if you want to enable this feature or discard all imported proposals in the `Participatory Texts` menu if you want to disable it. proposal_answering_enabled: Proposal answering enabled proposal_edit_before_minutes: Proposals can be edited by authors before this many minutes passes proposal_length: Maximum proposal body length @@ -109,6 +111,12 @@ en: threshold_per_proposal: Threshold per proposal vote_limit: Support limit per participant step: + amendment_creation_enabled: Amendment creation enabled + amendment_creation_enabled_help: Participant can amend proposals. + amendment_promotion_enabled: Amendment promotion enabled + amendment_promotion_enabled_help: Emandation authors will be able to promote to Proposal the rejected emendation. + amendment_reaction_enabled: Amendment reaction enabled + amendment_reaction_enabled_help: Proposal's authors will be able to accept or reject Participant's emendations. announcement: Announcement automatic_hashtags: Hashtags added to all proposals comments_blocked: Comments blocked @@ -687,6 +695,10 @@ en: other: and %{count} more people link_to_collaborative_draft_help_text: This proposal is the result of a collaborative draft. Review the history link_to_collaborative_draft_text: See the collaborative draft + link_to_promoted_emendation_help_text: This proposal is a promoted emendation + link_to_promoted_emendation_text: See the rejected emendation. + link_to_proposal_from_emendation_help_text: This is a rejected emendation + link_to_proposal_from_emendation_text: See the Proposal proposal_accepted_reason: 'This proposal has been accepted because:' proposal_in_evaluation_reason: This proposal is being evaluated proposal_rejected_reason: 'This proposal has been rejected because:'
decidim-proposals/lib/decidim/proposals/component.rb+3 −0 modified@@ -55,6 +55,9 @@ settings.attribute :comments_blocked, type: :boolean, default: false settings.attribute :creation_enabled, type: :boolean settings.attribute :proposal_answering_enabled, type: :boolean, default: true + settings.attribute :amendment_creation_enabled, type: :boolean, default: true + settings.attribute :amendment_reaction_enabled, type: :boolean, default: true + settings.attribute :amendment_promotion_enabled, type: :boolean, default: true settings.attribute :announcement, type: :text, translated: true, editor: true settings.attribute :automatic_hashtags, type: :text, editor: false, required: false settings.attribute :suggested_hashtags, type: :text, editor: false, required: false
decidim-proposals/spec/lib/decidim/proposals/component_spec.rb+50 −17 modified@@ -33,7 +33,7 @@ end describe "on update" do - context "when trying to change the `participatory_texts_enabled` setting" do + describe "participatory_texts_enabled" do let(:form) do instance_double( Decidim::Admin::ComponentForm, @@ -54,17 +54,15 @@ it "updates the component" do expect do Decidim::Admin::UpdateComponent.call(form, component) - end.to change { - component.settings.participatory_texts_enabled - }.from(false).to(true) + end.to broadcast(:ok) end end context "when there are proposals for the component" do let(:proposal) { create(:proposal, component: component) } let(:valid) { false } - it "does not update the component" do + it "does NOT update the component" do expect do Decidim::Admin::UpdateComponent.call(form, component) end.to broadcast(:invalid) @@ -151,36 +149,71 @@ let(:edit_component_path) do Decidim::EngineRouter.admin_proxy(component.participatory_space).edit_component_path(component.id) end - let(:participatory_texts_enabled_checkbox) do - page.find("input[name='component[settings][participatory_texts_enabled]']") - end before do switch_to_host(component.organization.host) login_as current_user, scope: :user end - context "when there are no proposals for the component" do + describe "participatory_texts_enabled" do + let(:participatory_texts_enabled) { page.find("input#component_settings_participatory_texts_enabled") } + before do visit edit_component_path end - it "ALLOWS the admin the enable the Participatory texts feature" do - expect(participatory_texts_enabled_checkbox[:class]).not_to include("disabled") + context "when there are no proposals for the component" do + it "allows to check the setting" do + expect(participatory_texts_enabled[:class]).not_to include("disabled") + end + + it "changes the setting value after updating" do + expect do # rubocop:disable Lint/AmbiguousBlockAssociation + check "Participatory texts enabled" + click_button "Update" + end.to change { component.reload.settings.participatory_texts_enabled } + end + end + + context "when there are proposals for the component" do + before do + component.update(settings: { participatory_texts_enabled: true }) # Testing from true to false + create(:proposal, component: component) + visit edit_component_path + end + + it "does NOT allow to check the setting" do + expect(participatory_texts_enabled[:class]).to include("disabled") + + expect(page).to have_content("Cannot interact with this setting if there are existing proposals. Please, create a new `Proposals component` if you want to enable this feature or discard all imported proposals in the `Participatory Texts` menu if you want to disable it.") + end + + it "does NOT change the setting value after updating" do + expect do # rubocop:disable Lint/AmbiguousBlockAssociation + click_button "Update" + end.not_to change { component.reload.settings.participatory_texts_enabled } + end end end - context "when there are proposals for the component" do + describe "amendments settings" do before do - create(:proposal, component: component) visit edit_component_path end - it "DOES NOT ALLOW the admin the enable the Participatory texts feature" do - expect(participatory_texts_enabled_checkbox[:class]).to include("disabled") + context "when amendments_enabled global setting is checked" do + before do + check "Amendments enabled" + end - within ".help-text" do - expect(page).to have_content("Cannot interact with this setting if there are existing proposals. Please, create a new `Proposals component` if you want to enable this feature or discard all imported proposals in the `Participatory Texts` menu if you want to disable it.") + it "is shown the amendments step settings" do + expect(page).to have_css(".amendments_step_settings", visible: true) + end + end + + context "when amendments_enabled global setting is NOT checked" do + it "is NOT shown the amendments step settings" do + expect(page).to have_css(".amendments_step_settings", visible: false) end end end
decidim-proposals/spec/system/amend_proposal_spec.rb+242 −135 modified@@ -3,182 +3,256 @@ require "spec_helper" describe "Amend Proposal", versioning: true, type: :system do - include_context "with a component" - let(:manifest_name) { "proposals" } - - let!(:proposal) { create(:proposal, title: "Title", body: "One liner body", component: component) } - let!(:emendation) { create(:proposal, body: "Amended One liner body", component: component) } - let!(:amendment) { create :amendment, amendable: proposal, emendation: emendation } - let!(:user) { create :user, :confirmed, organization: organization } - let!(:user_group) { create(:user_group, :verified, organization: organization, users: [user]) } + let!(:component) { create(:proposal_component) } + let!(:proposal) { create(:proposal, title: "Long enough title", body: "One liner body", component: component) } + let!(:emendation) { create(:proposal, title: "Amended Long enough title", body: "Amended One liner body", component: component) } + let!(:amendment) { create :amendment, amendable: proposal, emendation: emendation, amender: emendation.creator_author } let(:emendation_path) { Decidim::ResourceLocatorPresenter.new(emendation).path } + let(:proposal_path) { Decidim::ResourceLocatorPresenter.new(proposal).path } + + def update_component_step_setting(component, step_setting_name, value) + component.update!( + step_settings: { + component.participatory_space.active_step.id => { + step_setting_name => value + } + } + ) + end - context "when amendments are not enabled" do - it "doesn't show the amend proposal button" do - visit_component - - click_link proposal.title - expect(page).to have_no_link("Amend Proposal") - end + before do + switch_to_host(component.organization.host) end - context "when amendments are enabled" do - let!(:component) do - create(:proposal_component, - :with_amendments_enabled, - manifest: manifest, - participatory_space: participatory_process) + context "with existing amendments" do + context "when visiting an amended proposal" do + before do + visit proposal_path + end + + it "is shown the amendments list" do + expect(page).to have_css("#amendments", text: "AMENDMENTS") + within ".amendment-list" do + expect(page).to have_content(emendation.title) + end + end + + it "is shown the amenders list" do + expect(page).to have_content("AMENDED BY") + within ".amender-list" do + expect(page).to have_content(emendation.creator_author.name) + end + end end - context "and visits an amendable proposal" do + context "when visiting an amendment to a proposal" do before do - visit_component - click_link proposal.title + visit emendation_path end - it "renders a link to Amend it" do - expect(page).to have_link("Amend Proposal") - end + it "shows the changed attributes" do + expect(page).to have_content("Amendment to \"#{proposal.title}\"") - context "when the user is not logged in and clicks" do - it "is shown the login modal" do - within ".card__amend-button", match: :first do - click_link "Amend Proposal" + within ".diff-for-title" do + expect(page).to have_content("TITLE") + + within ".diff > ul > .del" do + expect(page).to have_content(proposal.title) end - expect(page).to have_css("#loginModal", visible: true) + within ".diff > ul > .ins" do + expect(page).to have_content(emendation.title) + end end - end - context "when the user is logged in and clicks" do - before do - login_as user, scope: :user - visit_component - click_link proposal.title - end + within ".diff-for-body" do + expect(page).to have_content("BODY") - it "is shown the amend form" do - within ".card__amend-button", match: :first do - click_link "Amend Proposal" + within ".diff > ul > .del" do + expect(page).to have_content(proposal.body) end - expect(page).to have_css(".new_amendment", visible: true) - expect(page).to have_content("CREATE YOUR AMENDMENT") + within ".diff > ul > .ins" do + expect(page).to have_content(emendation.body) + end end end + end + end - context "when creating an amendment" do + context "with amendments NOT enabled" do + before do + component.update!(settings: { amendments_enabled: false }) + end + + context "when amendment CREATION is enabled" do + before do + update_component_step_setting(component, :amendment_creation_enabled, true) + end + + context "and visits an amendable proposal" do before do - login_as user, scope: :user - visit decidim.new_amend_path(amendable_gid: proposal.to_sgid.to_s) + visit proposal_path end - it "is shown the amend title field" do - expect(page).to have_css(".field", text: "Title", visible: true) - end - it "is shown the amend body field" do - expect(page).to have_css(".field", text: "Body", visible: true) - end - it "is shown the amend user group as field" do - expect(page).to have_css(".field", text: "Amendment author", visible: true) - end - it "is shown the submit button" do - expect(page).to have_button("Send amendment") + it "is NOT shown a link to Amend it" do + expect(page).not_to have_link("Amend Proposal") end end + end + + context "when amendment REACTION is enabled" do + before do + update_component_step_setting(component, :amendment_reaction_enabled, true) + end + + context "and the proposal author visits an emendation to their proposal" do + let(:user) { proposal.creator_author } - context "when the form is filled correctly" do before do login_as user, scope: :user - visit decidim.new_amend_path(amendable_gid: proposal.to_sgid.to_s) - within ".new_amendment" do - fill_in "amendment[emendation_params][title]", with: "More sidewalks and less roads" - fill_in "amendment[emendation_params][body]", with: "Cities need more people, not more cars" - select user_group.name, from: :amendment_user_group_id - end - click_button "Send amendment" + visit emendation_path end - it "is shown the Success Callout" do - expect(page).to have_css(".callout.success", text: "The amendment has been created successfully") + it "is NOT shown the accept and reject button" do + expect(page).not_to have_css(".success", text: "ACCEPT") + expect(page).not_to have_css(".alert", text: "REJECT") end + end + end - it "is shown the emendation in the amendments list" do - emendation = Decidim::Proposals::Proposal.last - - expect(page).to have_content(emendation.title) - expect(page).to have_content(emendation.body) - expect(page).to have_css(".card__text--status", text: "EVALUATING") - end + context "when amendment PROMOTION is enabled" do + before do + update_component_step_setting(component, :amendment_promotion_enabled, true) end - context "when the form is filled incorrectly" do + context "and the author of a rejected emendation visits their emendation" do + let(:user) { emendation.creator_author } + before do + amendment.update(state: "rejected") login_as user, scope: :user - visit decidim.new_amend_path(amendable_gid: proposal.to_sgid.to_s) - within ".new_amendment" do - fill_in "amendment[emendation_params][title]", with: "INVALID TITLE" - end - click_button "Send amendment" - end - - it "is shown the Error Callout" do - expect(page).to have_css(".callout.alert", text: "An error ocurred while creating the amendment") + visit emendation_path end - it "is shown the error message" do - expect(page).to have_css(".form-error.is-visible", text: "Title is using too many capital letters") + it "is NOT shown the promote button" do + expect(page).not_to have_content("PROMOTE TO PROPOSAL") + expect(page).not_to have_content("You can promote this emendation and publish it as an independent proposal") end end end + end + + context "with amendments enabled" do + before do + component.update!(settings: { amendments_enabled: true }) + end - context "when viewing an amendment" do + context "when amendment CREATION is enabled" do before do - visit_component - login_as user, scope: :user - click_link emendation.title + update_component_step_setting(component, :amendment_creation_enabled, true) end - it "shows the changed attributes" do - expect(page).to have_content("Amendment to \"#{proposal.title}\"") + context "and visits an amendable proposal" do + before do + visit proposal_path + end - within ".diff-for-title" do - expect(page).to have_content("TITLE") + it "is shown a link to Amend it" do + expect(page).to have_link("Amend Proposal") + end - within ".diff > ul > .del" do - expect(page).to have_content(proposal.title) - end + context "when the user is not logged in and clicks" do + it "is shown the login modal" do + within ".card__amend-button", match: :first do + click_link "Amend Proposal" + end - within ".diff > ul > .ins" do - expect(page).to have_content(emendation.title) + expect(page).to have_css("#loginModal", visible: true) end end - within ".diff-for-body" do - expect(page).to have_content("BODY") + context "when the user is logged in and clicks" do + let!(:user) { create :user, :confirmed, organization: component.organization } + let!(:user_group) { create(:user_group, :verified, organization: user.organization, users: [user]) } - within ".diff > ul > .del" do - expect(page).to have_content(proposal.body) + before do + login_as user, scope: :user + visit proposal_path + click_link "Amend Proposal" end - within ".diff > ul > .ins" do - expect(page).to have_content(emendation.body) + it "is shown the amendment create form" do + expect(page).to have_css(".new_amendment", visible: true) + expect(page).to have_content("CREATE YOUR AMENDMENT") + expect(page).to have_css(".field", text: "Title", visible: true) + expect(page).to have_css(".field", text: "Body", visible: true) + expect(page).to have_css(".field", text: "Amendment author", visible: true) + expect(page).to have_button("Send amendment") + end + + context "when the form is filled correctly" do + before do + within ".new_amendment" do + fill_in "amendment[emendation_params][title]", with: "More sidewalks and less roads" + fill_in "amendment[emendation_params][body]", with: "Cities need more people, not more cars" + select user_group.name, from: :amendment_user_group_id # Optional + end + click_button "Send amendment" + end + + it "is shown the Success Callout" do + expect(page).to have_css(".callout.success", text: "The amendment has been created successfully") + end + end + + context "when the form is filled incorrectly" do + before do + within ".new_amendment" do + fill_in "amendment[emendation_params][title]", with: "INVALID TITLE" + end + click_button "Send amendment" + end + + it "is shown the Error Callout" do + expect(page).to have_css(".callout.alert", text: "An error ocurred while creating the amendment") + end + + it "is shown the field error message" do + expect(page).to have_css(".form-error.is-visible", text: "Title is using too many capital letters") + end end end end end - context "when the user is the author of the amendable proposal" do - let(:user) { proposal.creator_author } + context "when amendment CREATION is NOT enabled" do + before do + update_component_step_setting(component, :amendment_creation_enabled, false) + end + + context "and visits an amendable proposal" do + before do + visit proposal_path + end + + it "is NOT shown a link to Amend it" do + expect(page).not_to have_link("Amend Proposal") + end + end + end + context "when amendment REACTION is enabled" do before do - visit_component - login_as user, scope: :user + update_component_step_setting(component, :amendment_reaction_enabled, true) end - context "and visits an emendation to their proposal" do + context "and the proposal author visits an emendation to their proposal" do + let(:user) { proposal.creator_author } + before do - click_link emendation.title + login_as user, scope: :user + visit emendation_path end it "is shown the accept and reject button" do @@ -188,7 +262,7 @@ context "when the user clicks on the accept button" do before do - visit decidim.review_amend_path(amendment) + click_link "Accept" end it "shows the changed attributes" do @@ -217,11 +291,11 @@ end end - it "is shown the review the amendment form" do + it "is shown the amendment review form" do expect(page).to have_css(".edit_amendment") expect(page).to have_content("REVIEW THE AMENDMENT") - expect(page).to have_field("Title", with: emendation.title.to_s) - expect(page).to have_field("Body", with: emendation.body.to_s) + expect(page).to have_field("Title", with: emendation.title) + expect(page).to have_field("Body", with: emendation.body) expect(page).to have_button("Accept amendment") end @@ -237,47 +311,59 @@ end it "is changed the state of the emendation" do - visit_component - - within "#proposal_#{emendation.id}" do - expect(page).to have_css(".success", text: "ACCEPTED") - end + expect(page).to have_css(".success", text: "This amendment for the proposal #{emendation.title} has been accepted") end end end context "when the user clicks on the reject button" do before do - find("a[href='#{decidim.reject_amend_path(amendment)}']").click + click_link "Reject" end it "is shown the Success Callout" do expect(page).to have_css(".callout.success", text: "The amendment has been successfully rejected") end it "is changed the state of the emendation" do - visit_component - - within "#proposal_#{emendation.id}" do - expect(page).to have_css(".alert", text: "REJECTED") - end + expect(page).to have_css(".alert", text: "This amendment for the proposal #{proposal.title} was rejected") end end end end - context "when the user is the author of the emendation" do - let(:user) { emendation.creator_author } - let!(:amendment) { create :amendment, amendable: proposal, emendation: emendation, state: "rejected" } + context "when amendment REACTION is NOT enabled" do + before do + update_component_step_setting(component, :amendment_reaction_enabled, false) + end + + context "and the proposal author visits an emendation to their proposal" do + let(:user) { proposal.creator_author } + + before do + login_as user, scope: :user + visit emendation_path + end + + it "is NOT shown the accept and reject button" do + expect(page).not_to have_css(".success", text: "ACCEPT") + expect(page).not_to have_css(".alert", text: "REJECT") + end + end + end + context "when amendment PROMOTION is enabled" do before do - visit_component - login_as user, scope: :user + update_component_step_setting(component, :amendment_promotion_enabled, true) end - context "and visits a rejected emendation" do + context "and the author of a rejected emendation visits their emendation" do + let(:user) { emendation.creator_author } + before do - click_link emendation.title + amendment.update(state: "rejected") + login_as user, scope: :user + visit emendation_path end it "is shown the promote button" do @@ -287,7 +373,7 @@ context "when the user clicks on the promote button" do before do - find("a[href='#{decidim.promote_amend_path(amendment)}']").click + click_link "Promote" end it "is shown the alert text" do @@ -313,5 +399,26 @@ end end end + + context "when amendment PROMOTION is NOT enabled" do + before do + update_component_step_setting(component, :amendment_promotion_enabled, false) + end + + context "and the author of a rejected emendation visits their emendation" do + let(:user) { emendation.creator_author } + + before do + amendment.update(state: "rejected") + login_as user, scope: :user + visit emendation_path + end + + it "is NOT shown the promote button" do + expect(page).not_to have_content("PROMOTE TO PROPOSAL") + expect(page).not_to have_content("You can promote this emendation and publish it as an independent proposal") + end + end + end end end
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/decidim/decidim/commit/1b99136a1c7aa02616a0b54a6ab88d12907a57a9nvdPatchWEB
- github.com/advisories/GHSA-w5xj-99cg-rccmghsaADVISORY
- github.com/decidim/decidim/security/advisories/GHSA-w5xj-99cg-rccmnvdMitigationVendor AdvisoryPatchWEB
- nvd.nist.gov/vuln/detail/CVE-2026-40869ghsaADVISORY
News mentions
0No linked articles in our index yet.