Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Amazon SES/SNS ingress to ActionMailbox #39364

Closed
wants to merge 10 commits into from

Conversation

bobf
Copy link
Contributor

@bobf bobf commented May 20, 2020

Summary

Provides an ingress for Amazon SES/SNS.

Includes:

  • /rails/action_mailbox/amazon/inbound_emails ingress route
  • Signature verification
  • Auto-subscribe
  • Permitted SNS topic configuration

Tests for all of the above are included.

Other Information

This commit (f480cfa) from @georgeclaghorn states that SES integration was removed and will be re-written for 6.1.

I needed this sooner so I wrote a gem here: https://github.com/bobf/action_mailbox_amazon_ingress

I emailed George to discuss and he suggested a PR. I have adapted the tests from RSpec to Minitest.

Please let me know if any adjustments are needed.

@bobf
Copy link
Contributor Author

bobf commented May 20, 2020

I've just deployed an app running Rails from this branch and set up an SES/SNS topic. Confirmation and receipt both appear to be working fine.

@bobf bobf force-pushed the actionmailbox-amazon-ingress branch from 93c49bf to 78e4914 Compare May 20, 2020 13:38
@georgeclaghorn georgeclaghorn self-assigned this May 20, 2020
Copy link
Contributor

@georgeclaghorn georgeclaghorn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! Here’s the first round of feedback.

actionmailbox/actionmailbox.gemspec Outdated Show resolved Hide resolved
actionmailbox/config/routes.rb Outdated Show resolved Hide resolved
Comment on lines 98 to 100
def notification
@notification ||= JSON.parse(request.body.read)
rescue JSON::ParserError => e
Rails.logger.warn("Unable to parse SNS notification: #{e}")
nil
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to parse the request body manually, or can we rely on Rails parsing into params based on Content-Type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for this is that the signature verification requires the exact structure provided in the post body. The params object contains extra parameters (controller, action) that invalidate the signature. I opted to use the same object in all places rather than mixing params and my parsed object.

Is there a way to extract just the post body JSON-extracted params from the params object ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

params.except(:controller, :action) think that would be clearer too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kaspth I considered this but it seems quite flaky to me. If any extra "special" parameters are added to Rails then this code will need to be updated. Since the SNS verifier depends on the exact input parameters included in the POST body, this [my implementation] seems the most correct way to do it, albeit with an extra JSON.parse.

I'm happy to be overruled !

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't recall us having added any extra default params in the last …10 years? My memory is a bit spotty beyond that, but don't think it's happened. I think the clarity of not sidestepping the default params parsing is overall better.

Is this something we could exercise in a test? Perhaps even with just a no-op action with render json: params and does assert_empty params.except(:controller, :action)?

@bobf bobf force-pushed the actionmailbox-amazon-ingress branch from 646b903 to 058b88d Compare May 23, 2020 14:27
@bobf
Copy link
Contributor Author

bobf commented May 23, 2020

Thanks for the PR! Here’s the first round of feedback.

Thanks a lot for the feedback. I have addressed all of your points. I think there is only one item outstanding (pending your feedback).

Let me know if you prefer squashed commits when the PR is ready.

@kaspth
Copy link
Contributor

kaspth commented May 25, 2020

I still like @georgeclaghorn's idea of a separate ConfirmationsController. I also think that would force us to consider a model extraction.

Here's my take on that.
# frozen_string_literal: true

module ActionMailbox
  module Ingresses
    class Amazon::BaseController < ActionMailbox::BaseController
      before_action :set_notification

      private
        def set_notification
          @notification = SNSNotification.new params.except(:controller, :action)
        end

        class SNSNotification
          def initialize(params)
            @params = params
          end

          def subscription_confirmed?
            Net::HTTP.get_response(URI(params[:SubscribeURL])).code&.start_with?("2")
          end

          def verified?
            require "aws-sdk-sns"
            Aws::SNS::MessageVerifier.new.authentic?(params.to_json)
          end

          def topic
            params[:TopicArn]
          end

          def message_content
            params.dig(:Message, :content) if receipt?
          end

          private
            attr_reader :params

            def receipt?
              params[:Type] == "Notification" || params[:notificationType] == "Received"
            end
        end
    end

    class Amazon::ConfirmationsController < Amazon::BaseController
      def create
        if @notification.subscription_confirmed?
          head :ok
        else
          Rails.logger.error "SNS subscription confirmation request rejected."
          head :unprocessable_entity
        end
      end
    end

    class Amazon::InboundEmailsController < Amazon::BaseController
      before_action :ensure_message_content, :ensure_valid_topic, :ensure_verified

      def create
        ActionMailbox::InboundEmail.create_and_extract_message_id! @notification.message_content
      end

      private
        def ensure_message_content
          head :bad_request if @notification.message_content.blank?  
        end

        def ensure_valid_topic
          unless @notification.topic.in? Array(ActionMailbox.amazon.subscribed_topics)
            Rails.logger.warn "Ignoring unknown topic: #{@notification.topic}"
            head :unauthorized
          end
        end

        def ensure_verified
          head :unathorized unless @notification.verified?
        end
    end
  end
end

@bobf bobf force-pushed the actionmailbox-amazon-ingress branch from ab3448f to cf07c19 Compare May 25, 2020 19:47
@bobf
Copy link
Contributor Author

bobf commented May 25, 2020

I still like @georgeclaghorn's idea of a separate ConfirmationsController. I also think that would force us to consider a model extraction.

Here's my take on that.

@kaspth Thanks - agree this is much nicer. One minor point is that I think @georgeclaghorn suggested separate actions and not necessarily controllers (unless I have missed a message).

I think this is better either way but wanted to highlight just in case this made any difference.

I'll update to reflect your suggested changes tomorrow if there's no further discussion needed.

@kaspth
Copy link
Contributor

kaspth commented May 26, 2020

One minor point is that I think @georgeclaghorn suggested separate actions and not necessarily controllers (unless I have missed a message).

George suggested a separate controller in #39364 (comment) that's where I got the name from 😄

@georgeclaghorn
Copy link
Contributor

Sorry, I think “the different actions” in my comment may have confused. I meant “the two different actions in the separate controllers.”

@bobf bobf force-pushed the actionmailbox-amazon-ingress branch from 9c0d17c to a1e7579 Compare May 28, 2020 14:32
@bobf
Copy link
Contributor Author

bobf commented May 28, 2020

@kaspth @georgeclaghorn Thanks both for the feedback. I have adapted Kasper's refactor and addressed a couple of issues. A couple of things I think worth noting:

  • The Message param in the POST body is a JSON string that must be manually decoded. i.e. it is double-encoded. It contains the original email event data structure.
  • ActionMailbox::BaseController#ingress_name (called by #ensure_configured) does not expect a controller that is not named InboundEmailsController so I have defined ActionMailbox::Ingresses::Amazon::BaseController#ingress_name which just returns :amazon. This feels kludgey but I didn't want to modify the base controller method. (Otherwise subscription confirmations fail).

Let me know if any changes are needed.

@mwesigwa
Copy link

@bobf, I'd suggest adding some form of documentation, alerting users not to enable the raw_message_delivery option when using this Ingress. I found out the hard way after spending a couple of hours trying to get this solution to work. When you enable raw message delivery for an Amazon SQS endpoint, any Amazon SNS metadata is stripped from the published message and the message is sent as is which results in the confounding error saying the topic is being ignored since it does not exist in the sent message.

Here is the Amazon documntation link that finally explained this.

@bobf
Copy link
Contributor Author

bobf commented Jun 21, 2020

@mwesigwa Thanks for the idea - I was tempted to select this option myself so I'm sure others will as well.

I've updated with this commit: 809f53b

Let me know if you think it needs adjusting !

@mwesigwa
Copy link

The change is great! For what it's worth, I can confirm that the code worked as expected 🥇. Thank you for implementing this.

@joshkinabrew
Copy link

Really looking forward to this being released! Thanks for your great work @bobf

@bobf
Copy link
Contributor Author

bobf commented Jul 27, 2020

@georgeclaghorn Is there anything here that needs changing ? I have a conflict with the main branch but would rather wait until I had some feedback before fixing in case this gets left out in the cold.

The conflict is only tiny in this case so will be no trouble to resolve but some of the tests (e.g. routing) can be a bit of a pain to deal with if things start to diverge. Let me know how you want to proceed.

@ssunday
Copy link

ssunday commented Mar 13, 2024

@zzak Are you able to see exactly what the error is for https://github.com/rails/rails/actions/runs/8265465928/job/22611174061?pr=39364 ? I'm not familiar with how to find the exact logs/issue is. I tried but found zilch in terms of trace.

I wonder if this does belong with aws-sdk-rails rather than Rails itself 🤔 What determines what belongs inside Rails vs. not?

Thanks by the way!

@@ -1,5 +1,8 @@
# frozen_string_literal: true

gem "aws-sdk-s3"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

based on this on the storage s3 service

raise LoadError,
"Could not load the 'aws-sdk-sns' gem. Ensure that you've added the gem to your Gemfile.", e.backtrace
end
require "aws-sdk-sns"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this since it seemed like it wasn't being done elsewhere with these type of requirements

@bobf
Copy link
Contributor Author

bobf commented Mar 13, 2024

@ssunday Thanks again for picking this up, and apologies for the drive-by collab invite - saw that you were active and blocked by what looked like a very unamusing PR so wanted to unblock ASAP but was in the middle of a crisis at work so could only spare a few seconds to do the bare minimum. Looking forward to getting this merged, wherever it ends up !

@ssunday
Copy link

ssunday commented Mar 13, 2024

@bobf Oh jeez, no worries, you gave me what I needed, haha. Thank you! Also eager to get this merged too.

bobf and others added 8 commits March 26, 2024 13:20
commit bf2df3a5f5cb3490213b63cf714deceb95b5a8cd
Author: Bob Farrell <git@bob.frl>
Date:   Sun May 28 11:58:13 2023 +0100

    Add Amazon SES/SNS ingress to ActionMailbox

        Squashed commit of the following:

        commit f055de1216dc56e3a0a7c664984d2c781d2650ae (HEAD -> actionmailbox-amazon-ingress)
        Author: Bob Farrell <git@bob.frl>
        Date:   Sun Jun 11 08:21:37 2023 +0100

            Rubocop fixes

        commit 2925d4b8f895f833c85fb966e250d74763355701 (actionmailbox-amazon-ingress)
        Author: Bob Farrell <git@bob.frl>
        Date:   Sun Jun 11 08:09:36 2023 +0100

            Apply changes from PR feedback from @rafaelfranca

            Fix scoping of documentation comment for AmazonSes module.

            Remove duplicate entry iin documentation.

            Provide description of AWS SES fixture signing Rake task.

            Remove obsolete configuration definition.

            Move `aws-sdk-s3` loader to test helper.

            Move `aws-sdk-sns` loader to top of
            `ActionMailbox::Ingresses::AmazonSes::SnsNotification` file and add
            LoadError message.

        commit 1453a5f
        Merge: a56e22d d75fdd1
        Author: Bob Farrell <git@bob.frl>
        Date:   Sun Jan 22 14:23:43 2023 +0000

            Merge branch 'main' into actionmailbox-amazon-ingress

        commit a56e22d
        Merge: ff285d5 6d91791
        Author: Bob Farrell <git@bob.frl>
        Date:   Sat Jan 21 20:17:08 2023 +0000

            Merge branch 'main' into actionmailbox-amazon-ingress

        commit ff285d5
        Author: Bob Farrell <git@bob.frl>
        Date:   Sat Jan 21 16:47:00 2023 +0000

            Code review feedback from @jeremy

            Rename ingress from `:amazon` to `:amazon_ses` for futureproofing.

            Misc. refactoring (create separate controller for subscriptions, handle
            unsubscribe notifications, improve robustness of JSON parsing, etc.).

            Improve documentation and CHANGELOG for clarity.

            Require SES emails to be saved to S3 to allow > 150k email content size.

        commit 6102329
        Merge: 10f2a49 6bace37
        Author: Bob Farrell <git@bob.frl>
        Date:   Sat Jan 21 13:39:58 2023 +0000

            Merge branch 'main' into actionmailbox-amazon-ingress

        commit 10f2a49
        Author: Chris Ortman <chris-ortman@uiowa.edu>
        Date:   Wed Dec 2 14:15:12 2020 -0600

            Fixes style offenses

        commit a637f7d
        Author: Chris Ortman <chris-ortman@uiowa.edu>
        Date:   Wed Dec 2 10:19:00 2020 -0600

            Prevent constant caching due to usage in routes.

            This fixes the zietwerk tests that checks for unloadable constants.

        commit 168bcd6
        Author: Chris Ortman <chris-ortman@uiowa.edu>
        Date:   Tue Nov 17 11:22:25 2020 -0600

            Adds support for SES message delivery via S3

            SES supports embedding messages in the SNS notification or storing
            the message contents in S3 for later retrieval.

            Messages embedded in SNS notifications have a maximum size of 150kb
            [source](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-action-sns.html)

            Messages embedded in SNS also do not support attachments.

            In order to support large messages and attachments the mail message
            can be stored in S3 and retrieved using the url embedded in the SNS
            notification.

        commit 15127be
        Author: Chris Ortman <chris-ortman@uiowa.edu>
        Date:   Tue Nov 17 11:18:23 2020 -0600

            Add rake task for signing AWS fixtures

            `rake sign_aws_fixtures`

            Will update the signature in the AWS fixtures using our own public
            and private key files

            This allows the fixture files to be created by hand rather
            than captured from real AWS traffic because the signature no longer
            'needs amazon's private key

            Keyfiles can be rotated by deleting them and re-running the task

        commit 4c0108a
        Author: Chris Ortman <chris-ortman@uiowa.edu>
        Date:   Wed Aug 26 15:03:18 2020 -0500

            Deal with text/plain content type from AWS

            Because AWS SNS only allows you to specify a single endpoint for
            a subscription, the same URL must be used to handle both confirmation
            and message handling.

            In order to correctly route the request to the right controller
            for each of these scenarios we need to look at the type of the
            message that AWS is giving us.

            Unfortunately, AWS sends these messages as text/plain so we can't
            rely on rails to parse the JSON and need to instead look at headers.

            Because the mimetype is text/plain we must need to manually parse
            the SNS body instead of relying on rails params

        commit 30de80d
        Author: Bob Farrell <git@bob.frl>
        Date:   Sun Jun 21 10:32:17 2020 +0100

            Remove misplaced markdown

        commit 25f9255
        Author: Bob Farrell <git@bob.frl>
        Date:   Sun Jun 21 10:28:31 2020 +0100

            Warn about Amazon SES "raw message delivery" option

            Add documentation to explain that "raw message delivery" in AWS should
            not be enabled for Amazon SES/SNS ActionMailbox integration.

            As suggested by @mwesigwa:
            rails#39364 (comment)

        commit 2e43d84
        Author: Bob Farrell <git@bob.frl>
        Date:   Thu May 28 15:04:29 2020 +0100

            Refactor Amazon ingress logic to separate controllers

            Fix misc. routes-related Railties tests.

        commit 62b931a
        Author: Bob Farrell <git@bob.frl>
        Date:   Mon May 25 21:12:39 2020 +0100

            Fix Railties routing tests

            Include new Amazon ActionMailbox `subscribe` action.

        commit ddc2a05
        Author: Bob Farrell <git@bob.frl>
        Date:   Mon May 25 20:32:42 2020 +0100

            Simplify Amazon ActionMailbox testing

            Use Rails `as: :json` instead of manually encoding/setting headers.

        commit 5881ba7
        Author: Bob Farrell <git@bob.frl>
        Date:   Sat May 23 15:51:38 2020 +0100

            Use constraints to separate Amazon notification types

        commit c0e9867
        Author: Bob Farrell <git@bob.frl>
        Date:   Sat May 23 14:59:56 2020 +0100

            Misc documentation updates

            Addresses PR feedback:

              rails#39364 (comment)
              rails#39364 (comment)

        commit 8dddbe7
        Author: bobf <robertanthonyfarrell@gmail.com>
        Date:   Sat May 23 14:33:16 2020 +0100

            Update actionmailbox/config/routes.rb

            Co-authored-by: George Claghorn <george.claghorn@gmail.com>

        commit cfd13e8
        Author: bobf <robertanthonyfarrell@gmail.com>
        Date:   Sat May 23 14:32:58 2020 +0100

            Update actionmailbox/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb

            Co-authored-by: George Claghorn <george.claghorn@gmail.com>

        commit a9020f4
        Author: bobf <robertanthonyfarrell@gmail.com>
        Date:   Sat May 23 14:32:39 2020 +0100

            Update actionmailbox/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb

            Co-authored-by: George Claghorn <george.claghorn@gmail.com>

        commit 266203c
        Author: bobf <robertanthonyfarrell@gmail.com>
        Date:   Sat May 23 14:32:23 2020 +0100

            Update actionmailbox/actionmailbox.gemspec

            Co-authored-by: George Claghorn <george.claghorn@gmail.com>

        commit 8ae4bad
        Author: Chris Ortman <chris-ortman@uiowa.edu>
        Date:   Thu Dec 3 08:14:04 2020 -0600

            Add Amazon SES/SNS ingress to ActionMailbox
@ssunday ssunday force-pushed the actionmailbox-amazon-ingress branch 2 times, most recently from a722b28 to 0bf8300 Compare March 26, 2024 18:31
@ssunday ssunday force-pushed the actionmailbox-amazon-ingress branch from 0bf8300 to 571e623 Compare March 26, 2024 19:04
@ssunday
Copy link

ssunday commented Mar 26, 2024

I moved the non-controller files to lib and lazy-loaded the file so rails doesn't melt on launch if the aws-sdk-sns is not part of the bundle (which was what was causing the rails-new-docker step to fail a lot).

I haven't ever tested this code in the wild to make sure it works e2e, assuming test and previous work covers that.

Added myself as part of the authors for this work 😂

@ssunday
Copy link

ssunday commented Mar 26, 2024

Failure looks flaky, when I rebase next I assume it will resolve.

@ssunday
Copy link

ssunday commented May 9, 2024

@bobf Okay, I have confirmed this is working in the real world haha (without doing a rebase). Can you re-request review from folks? I can figure out how to contact people if not.

@bobf
Copy link
Contributor Author

bobf commented May 9, 2024

@zzak I don't see your name in the reviewers list, are you the right person to pass this to ?

@ssunday Thanks again for the work on this. 🙏

@ssunday
Copy link

ssunday commented May 9, 2024

@bobf No problem!

For reviewers: the CI failure definitely looks flaky/unrelated. I can rebase if everything else looks OK.

@rafaelfranca
Copy link
Member

Thank you for the pull request and sorry for the delay on reaching a decision.

After talking with other members of the Rails Core, I decided to not accept this pull request. We don't use Amazon SES/SNS in our applications and I don't feel we are the best maintainers for this code. This could live in a plugin for Rails like the one the author of this pull request already published..

We are happy to improve the system to allow plugins like those to exist if it isn't possible right now, but looking at the linked library it seems it already is.

Thank you again for you contributions to the community.

@bobf
Copy link
Contributor Author

bobf commented May 14, 2024

Just under a week short of the pull request's 4th birthday. :)

Thanks for considering the PR and the explanation, I understand you have to limit inflation to keep the framework manageable. ActionMailbox is indeed quite well suited for extending through plugins so I'm happy to go that route.

Have a nice week !

@mullermp
Copy link
Contributor

@bobf I'd love to get this into aws-sdk-rails. Would you like to open a PR and I can help shepherd that in?

@bobf
Copy link
Contributor Author

bobf commented May 15, 2024

@mullermp I think that would be a sensible path forward here, and I have no objections at all.

Maybe pick this up in this conversation: bobf/action_mailbox_amazon_ingress#14 (comment) - @ssunday has access to that repository so I believe would be able to make PRs, but I'm happy to help if needed, just might be a little slow to respond this week so trying to prevent myself being a blocker.

Thanks for your interest and enthusiasm, it sounds like ActionMailbox SES integration will find a good home. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet