diff --git a/.github/test-env-configs.json b/.github/test-env-configs.json index 88d25f8..09ce17b 100644 --- a/.github/test-env-configs.json +++ b/.github/test-env-configs.json @@ -12,7 +12,8 @@ "DISABLE_POKECODE_FOOTER": "", "DISABLE_LANGUAGE_MENU": "", "DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "", - "ALLOWED_RECIPIENTS": "" + "ALLOWED_RECIPIENTS": "", + "DISABLE_INVITATIONS": "" }, { "env-config": "all-disabled", @@ -27,7 +28,8 @@ "DISABLE_POKECODE_FOOTER": "1", "DISABLE_LANGUAGE_MENU": "1", "DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "1", - "ALLOWED_RECIPIENTS": "recipient1@example.com recipient2@example.com" + "ALLOWED_RECIPIENTS": "recipient1@example.com recipient2@example.com", + "DISABLE_INVITATIONS": "true" }, { "env-config": "footer-only", @@ -42,7 +44,8 @@ "DISABLE_POKECODE_FOOTER": "", "DISABLE_LANGUAGE_MENU": "1", "DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "1", - "ALLOWED_RECIPIENTS": "" + "ALLOWED_RECIPIENTS": "", + "DISABLE_INVITATIONS": "" }, { "env-config": "language-menu-only", @@ -57,7 +60,8 @@ "DISABLE_POKECODE_FOOTER": "1", "DISABLE_LANGUAGE_MENU": "", "DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "1", - "ALLOWED_RECIPIENTS": "" + "ALLOWED_RECIPIENTS": "", + "DISABLE_INVITATIONS": "" }, { "env-config": "assembly-members-visible-only", @@ -72,7 +76,8 @@ "DISABLE_POKECODE_FOOTER": "1", "DISABLE_LANGUAGE_MENU": "1", "DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "", - "ALLOWED_RECIPIENTS": "recipient1@example.com recipient2@example.com" + "ALLOWED_RECIPIENTS": "recipient1@example.com recipient2@example.com", + "DISABLE_INVITATIONS": "" }, { "env-config": "admin-iframe-only", @@ -87,6 +92,7 @@ "DISABLE_POKECODE_FOOTER": "1", "DISABLE_LANGUAGE_MENU": "1", "DISABLE_ASSEMBLY_MEMBERS_VISIBLE": "1", - "ALLOWED_RECIPIENTS": "recipient1@example.com recipient2@example.com" + "ALLOWED_RECIPIENTS": "recipient1@example.com recipient2@example.com", + "DISABLE_INVITATIONS": "" } ] diff --git a/README.md b/README.md index df4c2b2..b764e4b 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ This plugin relies on the command `decidim:upgrade` to make sure common files ar | `AWS_FORCE_PATH_STYLE` | Certain providers do not support the bucket name as the subdomain of the AWS endpoint (ie: Contabo). Set to `true` if that's the case. | `false` | | | `CONTENT_SECURITY_POLICY` | Sets custom Content Security Policy headers for enhanced security. When set, it is added to the default CSP configuration. | `""` (disabled) | | | `ALLOWED_RECIPIENTS` | A list of emails or domains that must match in order to send an email, separated by spaces. For instance `@pokecode.net johnsmith@gmail.com`. Exact email addresses (without a leading `@`) must match the full recipient email, while domain patterns starting with `@` are matched as suffixes of the recipient email. Leave empty to disable any interception. | `""` | +| `DISABLE_INVITATIONS` | Prevents all invitation emails from being sent by intercepting emails with the `invitation-instructions` header. This is useful for development or testing environments. | `false` | | ## Installation diff --git a/lib/decidim/pokecode/mail_interceptor.rb b/app/mailers/decidim/pokecode/allowed_recipients_mail_interceptor.rb similarity index 98% rename from lib/decidim/pokecode/mail_interceptor.rb rename to app/mailers/decidim/pokecode/allowed_recipients_mail_interceptor.rb index 06268a7..7c370a6 100644 --- a/lib/decidim/pokecode/mail_interceptor.rb +++ b/app/mailers/decidim/pokecode/allowed_recipients_mail_interceptor.rb @@ -4,7 +4,7 @@ module Decidim module Pokecode # Email interceptor that filters outgoing emails based on allowed recipients. # Only allows emails to recipients that match the configured allowed list. - class MailInterceptor + class AllowedRecipientsMailInterceptor def self.delivering_email(message) return if allowed_recipients_empty? diff --git a/app/mailers/decidim/pokecode/disable_invitations_mail_interceptor.rb b/app/mailers/decidim/pokecode/disable_invitations_mail_interceptor.rb new file mode 100644 index 0000000..58255ac --- /dev/null +++ b/app/mailers/decidim/pokecode/disable_invitations_mail_interceptor.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Decidim + module Pokecode + # Email interceptor that prevents invitation emails from being sent. + # Disables all emails that contain the 'invitation-instructions' header. + class DisableInvitationsMailInterceptor + def self.delivering_email(message) + return unless Decidim::Pokecode.disable_invitations + + # Check if this is an invitation email + if message["invitation-instructions"].present? + Rails.logger.info "[Decidim::Pokecode] Invitation email prevented. Instructions: #{message["invitation-instructions"]}, To: #{message.to.join(", ")}" + message.perform_deliveries = false + end + end + end + end +end diff --git a/app/overrides/decidim/add_staging_warning.rb b/app/overrides/decidim/add_staging_warning.rb deleted file mode 100644 index 07a241e..0000000 --- a/app/overrides/decidim/add_staging_warning.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -if Decidim::Pokecode.allowed_recipients_list.any? - Deface::Override.new(:virtual_path => "decidim/admin/dashboard/show", - :name => "add-staging-warning", - :insert_before => "div.content", - :partial => "decidim/pokecode/staging_warning") -end diff --git a/app/views/decidim/pokecode/admin/_invitations_disabled_warning.html.erb b/app/views/decidim/pokecode/admin/_invitations_disabled_warning.html.erb new file mode 100644 index 0000000..9001d9b --- /dev/null +++ b/app/views/decidim/pokecode/admin/_invitations_disabled_warning.html.erb @@ -0,0 +1,6 @@ +
+

+ <%= t("decidim.pokecode.invitations_disabled_warning.title") %>
+ <%= t("decidim.pokecode.invitations_disabled_warning.message") %> +

+
diff --git a/app/views/decidim/pokecode/_staging_warning.html.erb b/app/views/decidim/pokecode/admin/_staging_warning.html.erb similarity index 100% rename from app/views/decidim/pokecode/_staging_warning.html.erb rename to app/views/decidim/pokecode/admin/_staging_warning.html.erb diff --git a/config/locales/en.yml b/config/locales/en.yml index f893e0e..9f9e06c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -5,3 +5,6 @@ en: staging_warning: title: "Warning: Staging Environment" message: "Emails sent from this instance may not reach their intended recipients. Allowed destinations are: %{emails}." + invitations_disabled_warning: + title: "Warning: Invitations Disabled" + message: "Invitation emails are currently disabled in this instance. Users will not receive invitation emails." diff --git a/lib/decidim/pokecode.rb b/lib/decidim/pokecode.rb index 4e96e76..7f2338f 100644 --- a/lib/decidim/pokecode.rb +++ b/lib/decidim/pokecode.rb @@ -3,9 +3,9 @@ require "decidim/pokecode/configuration" require "rails" require "decidim/core" +require "deface" require "health_check" if Decidim::Pokecode.health_check_enabled require "rails_semantic_logger" if Decidim::Pokecode.semantic_logger_enabled -require "deface" if Decidim::Pokecode.deface_enabled require "aws-sdk-s3" if Decidim::Pokecode.aws_cdn_host.present? require "decidim/pokecode/s3_object_override" if Decidim::Pokecode.aws_cdn_host.present? @@ -19,8 +19,6 @@ require "sidekiq/cron" end -require "decidim/pokecode/mail_interceptor" if Decidim::Pokecode.allowed_recipients_list.any? - require "decidim/pokecode/admin" require "decidim/pokecode/engine" require "decidim/pokecode/admin_engine" diff --git a/lib/decidim/pokecode/configuration.rb b/lib/decidim/pokecode/configuration.rb index a120730..6a22394 100644 --- a/lib/decidim/pokecode/configuration.rb +++ b/lib/decidim/pokecode/configuration.rb @@ -69,6 +69,10 @@ module Pokecode Decidim::Env.new("ALLOWED_RECIPIENTS", "").value end + config_accessor :disable_invitations do + Decidim::Env.new("DISABLE_INVITATIONS", false).present? + end + config_accessor :content_security_policies_extra do { "connect-src" => ENV.fetch("CONTENT_SECURITY_POLICY", "").split, @@ -94,10 +98,6 @@ def self.allowed_recipients_list Pokecode.allowed_recipients&.split(/[,\s]+/)&.reject(&:blank?) || [] end - def self.deface_enabled - Pokecode.pokecode_footer_enabled || Decidim::Pokecode.language_menu_enabled - end - def self.sentry_enabled Pokecode.sentry_dsn.present? end diff --git a/lib/decidim/pokecode/engine.rb b/lib/decidim/pokecode/engine.rb index 59d9aad..fc45a54 100644 --- a/lib/decidim/pokecode/engine.rb +++ b/lib/decidim/pokecode/engine.rb @@ -75,6 +75,23 @@ class Engine < ::Rails::Engine else Rails.logger.info "[Decidim::Pokecode] Active Storage CDN override disabled." end + + # Register deface overrides for admin dashboard warnings + if Decidim::Pokecode.allowed_recipients_list.any? + Deface::Override.new(:virtual_path => "decidim/admin/dashboard/show", + :name => "add-staging-warning", + :insert_before => "div.content", + :partial => "decidim/pokecode/admin/staging_warning") + Rails.logger.info "[Decidim::Pokecode] Staging warning deface override registered." + end + + if Decidim::Pokecode.disable_invitations + Deface::Override.new(:virtual_path => "decidim/admin/dashboard/show", + :name => "add-invitations-disabled-warning", + :insert_before => "div.content", + :partial => "decidim/pokecode/admin/invitations_disabled_warning") + Rails.logger.info "[Decidim::Pokecode] Invitations disabled warning deface override registered." + end end initializer "pokecode.zeitwerk_ignore_deface" do @@ -176,12 +193,24 @@ class Engine < ::Rails::Engine end initializer "pokecode.mail_interceptor" do - if Decidim::Pokecode.allowed_recipients_list.any? - config.action_mailer.interceptors ||= [] - config.action_mailer.interceptors << "Decidim::Pokecode::MailInterceptor" - Rails.logger.info "[Decidim::Pokecode] Email interceptor enabled. Allowed recipients: #{Decidim::Pokecode.allowed_recipients_list.join(", ")}" - else - Rails.logger.info "[Decidim::Pokecode] Email interceptor disabled." + Rails.application.config.to_prepare do + if Decidim::Pokecode.allowed_recipients_list.any? + unless ActionMailer::Base.try(:delivery_interceptors)&.include?(Decidim::Pokecode::AllowedRecipientsMailInterceptor) + ActionMailer::Base.register_interceptor(Decidim::Pokecode::AllowedRecipientsMailInterceptor) + end + Rails.logger.info "[Decidim::Pokecode] Allowed recipients mail interceptor enabled. Allowed recipients: #{Decidim::Pokecode.allowed_recipients_list.join(", ")}" + else + Rails.logger.info "[Decidim::Pokecode] Allowed recipients mail interceptor disabled." + end + + if Decidim::Pokecode.disable_invitations + unless ActionMailer::Base.try(:delivery_interceptors)&.include?(Decidim::Pokecode::DisableInvitationsMailInterceptor) + ActionMailer::Base.register_interceptor(Decidim::Pokecode::DisableInvitationsMailInterceptor) + end + Rails.logger.info "[Decidim::Pokecode] Invitations disabled via mail interceptor." + else + Rails.logger.info "[Decidim::Pokecode] Invitations not disabled via mail interceptor." + end end end end diff --git a/spec/lib/loaded_gems_spec.rb b/spec/lib/loaded_gems_spec.rb index ea8e685..ee3927f 100644 --- a/spec/lib/loaded_gems_spec.rb +++ b/spec/lib/loaded_gems_spec.rb @@ -48,16 +48,6 @@ module Decidim end end - if Decidim::Pokecode.deface_enabled - it "loads Deface" do - expect(defined?(Deface)).to be_truthy - end - else - it "does not load Deface" do - expect(defined?(Deface)).to be_falsey - end - end - if Decidim::Pokecode.aws_cdn_host.present? it "loads Aws::S3" do expect(Aws::S3::Object.included_modules).to include(Decidim::Pokecode::S3ObjectOverride) diff --git a/spec/mailers/disable_invitations_mail_interceptor_spec.rb b/spec/mailers/disable_invitations_mail_interceptor_spec.rb new file mode 100644 index 0000000..9e57a08 --- /dev/null +++ b/spec/mailers/disable_invitations_mail_interceptor_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::Pokecode + describe DisableInvitationsMailInterceptor do + let(:message) do + instance_double( + Mail::Message, + to: ["user@example.com"], + perform_deliveries: true + ) + end + + before do + allow(Rails.logger).to receive(:info) + allow(message).to receive(:perform_deliveries=) + allow(message).to receive(:[]).and_return(nil) + end + + describe ".delivering_email" do + context "when disable_invitations is disabled" do + before do + allow(Decidim::Pokecode).to receive(:disable_invitations).and_return(false) + end + + it "does not intercept any emails" do + allow(message).to receive(:[]).with("invitation-instructions").and_return("invite_private_user") + described_class.delivering_email(message) + expect(message).not_to have_received(:perform_deliveries=) + end + end + + context "when disable_invitations is enabled" do + before do + allow(Decidim::Pokecode).to receive(:disable_invitations).and_return(true) + end + + context "when the email has an invitation-instructions header" do + before do + allow(message).to receive(:[]).with("invitation-instructions").and_return("invite_private_user") + end + + it "prevents the email from being delivered" do + described_class.delivering_email(message) + expect(message).to have_received(:perform_deliveries=).with(false) + end + + it "logs the interception" do + described_class.delivering_email(message) + expect(Rails.logger).to have_received(:info).with( + include("[Decidim::Pokecode] Invitation email prevented") + ) + end + end + + context "when the email does not have an invitation-instructions header" do + before do + allow(message).to receive(:[]).with("invitation-instructions").and_return(nil) + end + + it "allows the email to be delivered" do + described_class.delivering_email(message) + expect(message).not_to have_received(:perform_deliveries=) + end + + it "does not log the interception" do + described_class.delivering_email(message) + expect(Rails.logger).not_to have_received(:info) + end + end + end + end + end +end diff --git a/spec/mailers/mail_interceptor_spec.rb b/spec/mailers/mail_interceptor_spec.rb index 972c512..cdede98 100644 --- a/spec/mailers/mail_interceptor_spec.rb +++ b/spec/mailers/mail_interceptor_spec.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true require "spec_helper" -require "decidim/pokecode/mail_interceptor" module Decidim::Pokecode - describe MailInterceptor do + describe AllowedRecipientsMailInterceptor do let(:message) do instance_double( Mail::Message, diff --git a/spec/system/allowed_recipients_spec.rb b/spec/system/mail_interceptor_spec.rb similarity index 58% rename from spec/system/allowed_recipients_spec.rb rename to spec/system/mail_interceptor_spec.rb index 74256d4..3630412 100644 --- a/spec/system/allowed_recipients_spec.rb +++ b/spec/system/mail_interceptor_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -describe "AllowedRecipients" do +describe "MailInterceptor" do let!(:organization) { create(:organization) } let!(:user) { create(:user, :confirmed, :admin, organization:) } @@ -24,4 +24,15 @@ expect(page).to have_no_content("Warning: Staging Environment") end end + + if Decidim::Pokecode.disable_invitations + it "shows the invitations disabled warning in the admin dashboard" do + expect(page).to have_content("Warning: Invitations Disabled") + expect(page).to have_content("Invitation emails are currently disabled in this instance. Users will not receive invitation emails.") + end + else + it "does not show the invitations disabled warning in the admin dashboard" do + expect(page).to have_no_content("Warning: Invitations Disabled") + end + end end