SECURITY: Handle concurrent invite accepts

Raise an error on concurrent invite accept attempts.
This commit is contained in:
Blake Erickson 2023-07-28 12:56:36 +01:00 committed by David Taylor
parent 439cc5b023
commit 6695d568ca
No known key found for this signature in database
GPG Key ID: 46904C18B1D3F434
3 changed files with 29 additions and 2 deletions

View File

@ -282,7 +282,10 @@ class InvitesController < ApplicationController
end
user = invite.redeem(**attrs)
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, Invite::UserExists => e
rescue ActiveRecord::RecordInvalid,
ActiveRecord::RecordNotSaved,
ActiveRecord::LockWaitTimeout,
Invite::UserExists => e
return render json: failed_json.merge(message: e.message), status: 412
end

View File

@ -243,7 +243,10 @@ class InviteRedeemer
@invited_user_record = InvitedUser.create!(invite_id: invite.id, redeemed_at: Time.zone.now)
if @invited_user_record.present?
Invite.increment_counter(:redemption_count, invite.id)
invite.with_lock("FOR UPDATE NOWAIT") do
Invite.increment_counter(:redemption_count, invite.id)
invite.save!
end
delete_duplicate_invites
end

View File

@ -980,6 +980,27 @@ RSpec.describe InvitesController do
Fabricate(:invite, email: nil, emailed_status: Invite.emailed_status_types[:not_required])
end
it "does not create multiple users for a single use invite" do
user_count = User.count
2
.times
.map do
Thread.new do
put "/invites/show/#{invite.invite_key}.json",
params: {
email: "test@example.com",
password: "verystrongpassword",
}
end
end
.each(&:join)
expect(invite.reload.max_redemptions_allowed).to eq(1)
expect(invite.reload.redemption_count).to eq(1)
expect(User.count).to eq(user_count + 1)
end
it "sends an activation email and does not activate the user" do
expect {
put "/invites/show/#{invite.invite_key}.json",