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

Mailer: Email to remind maintainers of gems with 180M+ downloads to enable MFA #3166

Merged
merged 9 commits into from
Aug 9, 2022
15 changes: 15 additions & 0 deletions app/mailers/mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,21 @@ def mfa_recommendation_announcement(user_id)
subject: "Please enable multi-factor authentication on your RubyGems account"
end

def mfa_required_soon_announcement(user_id)
@user = User.find(user_id)
case @user.mfa_level
when "disabled"
subject = "[Action Required] Enable multi-factor authentication on your RubyGems account by August 15"
@heading = "Enable multi-factor authentication on your RubyGems account"
when "ui_only"
subject = "[Action Required] Upgrade the multi-factor authentication level on your RubyGems account by August 15"
@heading = "Upgrade the multi-factor authentication level on your RubyGems account"
end

mail to: @user.email,
subject: subject
end

def gem_yanked(yanked_by_user_id, version_id, notified_user_id)
@version = Version.find(version_id)
notified_user = User.find(notified_user_id)
Expand Down
119 changes: 119 additions & 0 deletions app/views/mailer/mfa_required_soon_announcement.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<% @title = @heading %>
<% @sub_title = "πŸ‘‹ Hi #{@user.handle}" %>

<!-- Body -->
<table width="100%" border="0" cellspacing="0" cellpadding="0" bgcolor="#ffffff">
<tr>
<td class="content-spacing" style="font-size:0pt; line-height:0pt; text-align:left" width="20"></td>
<td>
<table width="100%" border="0" cellspacing="0" cellpadding="0" class="spacer" style="font-size:0pt; line-height:0pt; text-align:center; width:100%; min-width:100%"><tr><td height="35" class="spacer" style="font-size:0pt; line-height:0pt; text-align:center; width:100%; min-width:100%">&nbsp;</td></tr></table>

<table width="100%" border="0" cellspacing="0" cellpadding="0" class="spacer" style="font-size:0pt; line-height:0pt; text-align:center; width:100%; min-width:100%"><tr><td height="40" class="spacer" style="font-size:0pt; line-height:0pt; text-align:center; width:100%; min-width:100%">&nbsp;</td></tr></table>

<div class="h3-1-center" style="color:#1e1e1e; font-family:Georgia, serif; min-width:auto !important; font-size:20px; line-height:26px;">
<p>
Recently, we've <a href="https://blog.rubygems.org/2022/06/13/making-packages-more-secure.html" target="_blank">announced</a> our security-focused ambitions to the community.
</p>
<br/>

<p>
Next week, on August 15, 2022, we will begin requiring maintainers like yourself, who are owners of packages with more than 180 million downloads, to have multi-factor authentication (MFA) enabled.
</p>
<br/>

<p>
If you don't have MFA enabled by this date, a number of operations will be restricted.
These restrictions include editing profile pages on the web, signing in on the command line, and performing <a href="https://guides.rubygems.org/mfa-requirement-opt-in/#privileged-operations" target="_blank">privileged actions</a> (ie. push, yank, add/remove owners).
</p>
<br/>

<% if @user.mfa_disabled? %>
<p>
To avoid any disruptions with your RubyGems account, <b>please enable multi-factor authentication πŸ™.</b>
</p>
<br/>
<% elsif @user.mfa_ui_only? %>
<p>
We're notifying you of this policy because you have MFA enabled only for the UI.
<b>Please upgrade your RubyGems account to a stronger MFA level πŸ™.</b>
bettymakes marked this conversation as resolved.
Show resolved Hide resolved
</p>
<br/>

<p>
We recommend setting the
<a href="https://guides.rubygems.org/setting-up-multifactor-authentication/#authentication-levels" target="_blank">MFA level</a>
to <b><i>UI and API</i></b>. However, <b><i>UI and gem signin</i></b> is acceptable too.
</p>
<br />
<% end %>
<br/>

<p>Thank you for making the RubyGems ecosystem more secure.</p>
</div>

<table width="100%" border="0" cellspacing="0" cellpadding="0" class="spacer" style="font-size:0pt; line-height:0pt; text-align:center; width:100%; min-width:100%"><tr><td height="30" class="spacer" style="font-size:0pt; line-height:0pt; text-align:center; width:100%; min-width:100%">&nbsp;</td></tr></table>

<% if @user.mfa_disabled? || @user.mfa_ui_only? %>

<!-- Button -->
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center">
<table width="210" border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" bgcolor="#e9573f">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td class="img" style="font-size:0pt; line-height:0pt; text-align:left" width="15">
<table width="100%" border="0" cellspacing="0" cellpadding="0" class="spacer" style="font-size:0pt; line-height:0pt; text-align:center; width:100%; min-width:100%">
<tr>
<td height="50" class="spacer" style="font-size:0pt; line-height:0pt; text-align:center; width:100%; min-width:100%">&nbsp;</td>
</tr>
</table>
</td>
<td bgcolor="#e9573f">
<div class="text-btn" style="color:#ffffff; font-family:Arial, sans-serif; min-width:auto !important; font-size:16px; line-height:20px; text-align:center">
<a href=<%= new_multifactor_auth_url %> target="_blank" class="link-white" style="color:#ffffff; text-decoration:none">
<span class="link-white" style="color:#ffffff; text-decoration:none">
<%= "ENABLE MFA" if @user.mfa_disabled? %>
<%= "STRENGTHEN MFA LEVEL" if @user.mfa_ui_only? %>
</span>
</a>
</div>
</td>
<td class="img" style="font-size:0pt; line-height:0pt; text-align:left" width="15"></td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!-- END Button -->
<% end %>

<table width="100%" border="0" cellspacing="0" cellpadding="0" class="spacer" style="font-size:0pt; line-height:0pt; text-align:center; width:100%; min-width:100%"><tr><td height="40" class="spacer" style="font-size:0pt; line-height:0pt; text-align:center; width:100%; min-width:100%">&nbsp;</td></tr></table>

<!-- Adding a horizontal rule -->
<table width="100%">
<tr>
<td width="100%" bgcolor="#cccccc" height="1" style="font-size: 1px; line-height: 1px;">&nbsp;</td>
</tr>
</table>

<table width="100%" border="0" cellspacing="0" cellpadding="0" class="spacer" style="font-size:0pt; line-height:0pt; text-align:center; width:100%; min-width:100%"><tr><td height="40" class="spacer" style="font-size:0pt; line-height:0pt; text-align:center; width:100%; min-width:100%">&nbsp;</td></tr></table>

<div class="h3-1-center" style="color:#1e1e1e; font-family:Georgia, serif; min-width:auto !important; font-size:20px; line-height:26px; text-align:center">
Check our guides for more details on <%= link_to("setting up multi-factor authentication (MFA)", "https://guides.rubygems.org/setting-up-multifactor-authentication/") %> and <%= link_to("using multi-factor authentication (MFA) with command line", "https://guides.rubygems.org/using-mfa-in-command-line/") %>.
</div>

<table width="100%" border="0" cellspacing="0" cellpadding="0" class="spacer" style="font-size:0pt; line-height:0pt; text-align:center; width:100%; min-width:100%"><tr><td height="35" class="spacer" style="font-size:0pt; line-height:0pt; text-align:center; width:100%; min-width:100%">&nbsp;</td></tr></table>

<table width="100%" border="0" cellspacing="0" cellpadding="0" class="spacer" style="font-size:0pt; line-height:0pt; text-align:center; width:100%; min-width:100%"><tr><td height="35" class="spacer" style="font-size:0pt; line-height:0pt; text-align:center; width:100%; min-width:100%">&nbsp;</td></tr></table>

</td>
<td class="content-spacing" style="font-size:0pt; line-height:0pt; text-align:left" width="20"></td>
</tr>
</table>
<!-- END Body -->
19 changes: 19 additions & 0 deletions app/views/mailer/mfa_required_soon_announcement.text.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
πŸ‘‹ Hi <%= @user.handle %>,

Recently, we've announced our security-focused ambitions to the community (https://blog.rubygems.org/2022/06/13/making-packages-more-secure.html).

Next week, on August 15, 2022, we will begin requiring maintainers like yourself, who are owners of packages with more than 180 million downloads, to have multi-factor authentication (MFA) enabled.

If you don't have MFA enabled by this date, a number of operations will be restricted.
These restrictions include editing profile pages on the web, signing in on the command line, and performing privileged actions (ie. push, yank, add/remove owners) (https://guides.rubygems.org/mfa-requirement-opt-in/#privileged-operations).

<% if @user.mfa_disabled? %>
To avoid any disruptions with your RubyGems account, please enable multi-factor authentication πŸ™.
<% elsif @user.mfa_ui_only? %>
We're notifying you of this policy because you have MFA enabled only for the UI.
Please upgrade your RubyGems account to a stronger MFA level πŸ™.

We recommend setting the MFA level to "UI and API". However, "UI and gem signin" is acceptable too (https://guides.rubygems.org/setting-up-multifactor-authentication/#authentication-levels).
<% end %>

❀️ Thank you for making the RubyGems ecosystem more secure
19 changes: 19 additions & 0 deletions lib/tasks/mfa_policy.rake
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,23 @@ namespace :mfa_policy do
print format("\r%.2f%% (%d/%d) complete", i.to_f / total_users * 100.0, i, total_users)
end
end

# This task is meant to be run one week prior to MFA Phase 3 launch day - send out on Aug 8, 2022
# For more information on the MFA Phase 3 rollout, refer to this RFC:
# https://github.com/rubygems/rfcs/pull/36/files#diff-3d5cc3acc06fe7e9150fdbfc43399c5ad42572c122187774bfc3a4857df524f1R69-R85
# rake mfa_policy:reminder_enable_mfa
desc "Send email reminder to users who will have MFA enforced about impending MFA Phase 3 rollout"
task reminder_enable_mfa: :environment do
# users who own at least one gem with 180,000,000 downloads or more with weak or no MFA
users = User.joins(rubygems: :gem_download).where("gem_downloads.count >= 180000000").where(mfa_level: %w[disabled ui_only])
total_users = users.count
puts "Sending #{total_users} MFA reminder email"

i = 0
users.each do |user|
Mailer.delay.mfa_required_soon_announcement(user.id) if mx_exists?(user.email)
i += 1
print format("\r%.2f%% (%d/%d) complete", i.to_f / total_users * 100.0, i, total_users)
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 want to count and report failures?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. It wouldn't hurt if we did. I think the value of counting and reporting failures is if we expect someone will do something with that information. I'd defer this to whoever will be running the task, if they'd want to action on that info.

end
end
end
4 changes: 4 additions & 0 deletions test/mailers/previews/mailer_preview.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ def mfa_recommendation_announcement
Mailer.mfa_recommendation_announcement(User.last.id)
end

def mfa_required_soon_announcement
Mailer.mfa_required_soon_announcement(User.last.id)
end

def gem_yanked
ownership = Ownership.where.not(user: nil).last
Mailer.gem_yanked(ownership.user.id, ownership.rubygem.versions.last.id, ownership.user.id)
Expand Down
2 changes: 2 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ class ActionDispatch::IntegrationTest
Capybara.always_include_port = true
Capybara.server = :webrick

Gemcutter::Application.load_tasks

class SystemTest < ActionDispatch::IntegrationTest
include Capybara::DSL

Expand Down
65 changes: 61 additions & 4 deletions test/unit/mailer_test.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
require "test_helper"

class MailerTest < ActiveSupport::TestCase
class MailerTest < ActionMailer::TestCase
MIN_DOWNLOADS_FOR_MFA_RECOMMENDATION_POLICY = 165_000_000
MIN_DOWNLOADS_FOR_MFA_REQUIRED_POLICY = 180_000_000

context "sending mail for mfa recommendation announcement" do
setup do
@user = create(:user)
create(:rubygem, owners: [@user], downloads: MIN_DOWNLOADS_FOR_MFA_RECOMMENDATION_POLICY)

Gemcutter::Application.load_tasks
Rake::Task["mfa_policy:announce_recommendation"].invoke
@io_output, _error = capture_io { Rake::Task["mfa_policy:announce_recommendation"].execute }
Delayed::Worker.new.work_off
end

Expand All @@ -19,7 +19,64 @@ class MailerTest < ActiveSupport::TestCase
assert_equal [@user.email], email.to
assert_equal ["[email protected]"], email.from
assert_equal "Please enable multi-factor authentication on your RubyGems account", email.subject
assert_match "Recently, we've announced our security-focused ambitions to the community.", email.text_part.body.to_s
assert_match "Thank you for making the RubyGems ecosystem more secure", email.text_part.body.to_s
assert_match "Sending 1 MFA announcement email", @io_output
end
end

context "sending mail for mfa required soon announcement" do
bettymakes marked this conversation as resolved.
Show resolved Hide resolved
should "send mail to users with with more than 180M+ downloads and have MFA disabled" do
user = create(:user, mfa_level: "disabled")
create(:rubygem, owners: [user], downloads: MIN_DOWNLOADS_FOR_MFA_REQUIRED_POLICY)

@io_output, _error = capture_io { Rake::Task["mfa_policy:reminder_enable_mfa"].execute }
Delayed::Worker.new.work_off

refute_empty ActionMailer::Base.deliveries
bettymakes marked this conversation as resolved.
Show resolved Hide resolved
email = ActionMailer::Base.deliveries.last
assert_equal [user.email], email.to
assert_equal ["[email protected]"], email.from
assert_equal "[Action Required] Enable multi-factor authentication on your RubyGems account by August 15", email.subject
assert_match "Thank you for making the RubyGems ecosystem more secure", email.text_part.body.to_s
assert_match "Sending 1 MFA reminder email", @io_output
end

should "send mail to users with with more than 180M+ downloads and have weak MFA" do
user = create(:user, mfa_level: "ui_only")
create(:rubygem, owners: [user], downloads: MIN_DOWNLOADS_FOR_MFA_REQUIRED_POLICY)

@io_output, _error = capture_io { Rake::Task["mfa_policy:reminder_enable_mfa"].execute }
Delayed::Worker.new.work_off

refute_empty ActionMailer::Base.deliveries
email = ActionMailer::Base.deliveries.last
assert_equal [user.email], email.to
assert_equal ["[email protected]"], email.from
assert_equal "[Action Required] Upgrade the multi-factor authentication level on your RubyGems account by August 15", email.subject
assert_match "Recently, we've announced our security-focused ambitions to the community", email.text_part.body.to_s
assert_match "Sending 1 MFA reminder email", @io_output
end

should "not send mail to users with with more than 180M+ downloads and have strong MFA" do
user = create(:user, mfa_level: "ui_and_api")
create(:rubygem, owners: [user], downloads: MIN_DOWNLOADS_FOR_MFA_REQUIRED_POLICY)

@io_output, _error = capture_io { Rake::Task["mfa_policy:reminder_enable_mfa"].execute }
Delayed::Worker.new.work_off

assert_empty ActionMailer::Base.deliveries
assert_match "Sending 0 MFA reminder email", @io_output
end

should "not send mail to users with with less than 180M downloads" do
user = create(:user)
create(:rubygem, owners: [user], downloads: MIN_DOWNLOADS_FOR_MFA_REQUIRED_POLICY - 1)

@io_output, _error = capture_io { Rake::Task["mfa_policy:reminder_enable_mfa"].execute }
Delayed::Worker.new.work_off

assert_empty ActionMailer::Base.deliveries
assert_match "Sending 0 MFA reminder email", @io_output
end
end

Expand Down