SECURITY: Prevent email from being nil in InviteRedeemer (#19004)

This commit adds some protections in InviteRedeemer to ensure that email
can never be nil, which could cause issues with inviting the invited
person to private topics since there was an incorrect inner join.

If the email is nil and the invite is scoped to an email, we just use
that invite.email unconditionally.  If a redeeming_user (an existing
user) is passed in when redeeming an email, we use their email to
override the passed in email.  Otherwise we just use the passed in
email.  We now raise an error after all this if the email is still nil.
This commit also adds some tests to catch the private topic fix, and
some general improvements and comments around the invite code.

This commit also includes a migration to delete TopicAllowedUser records
for users who were mistakenly added to topics as part of the invite
redemption process.
This commit is contained in:
Martin Brennan 2022-11-14 12:02:06 +10:00 committed by GitHub
parent 78157b43ed
commit a414520742
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 525 additions and 232 deletions

View File

@ -760,7 +760,7 @@ class SessionController < ApplicationController
end end
if invite.redeemable? if invite.redeemable?
if !invite.is_invite_link? && sso.email != invite.email if invite.is_email_invite? && sso.email != invite.email
raise Invite::ValidationFailed.new(I18n.t("invite.not_matching_email")) raise Invite::ValidationFailed.new(I18n.t("invite.not_matching_email"))
end end
elsif invite.expired? elsif invite.expired?

View File

@ -74,8 +74,16 @@ class Invite < ActiveRecord::Base
end end
end end
# Even if a domain is specified on the invite, it still counts as
# an invite link.
def is_invite_link? def is_invite_link?
email.blank? self.email.blank?
end
# Email invites have specific behaviour and it's easier to visually
# parse is_email_invite? than !is_invite_link?
def is_email_invite?
self.email.present?
end end
def redeemable? def redeemable?
@ -201,8 +209,6 @@ class Invite < ActiveRecord::Base
) )
return if !redeemable? return if !redeemable?
email = self.email if email.blank? && !is_invite_link?
InviteRedeemer.new( InviteRedeemer.new(
invite: self, invite: self,
email: email, email: email,

View File

@ -1,5 +1,18 @@
# frozen_string_literal: true # frozen_string_literal: true
# NOTE: There are a _lot_ of complicated rules and conditions for our
# invite system, and the code is spread out through a lot of places.
# Tread lightly and read carefully when modifying this code. You may
# also want to look at:
#
# * InvitesController
# * SessionController
# * Invite model
# * User model
#
# Invites that are scoped to a specific email (email IS NOT NULL on the Invite
# model) have different rules to invites that are considered an "invite link",
# (email IS NULL) on the Invite model.
class InviteRedeemer class InviteRedeemer
attr_reader :invite, attr_reader :invite,
:email, :email,
@ -13,7 +26,7 @@ class InviteRedeemer
:redeeming_user :redeeming_user
def initialize( def initialize(
invite: nil, invite:,
email: nil, email: nil,
username: nil, username: nil,
name: nil, name: nil,
@ -23,9 +36,7 @@ class InviteRedeemer
session: nil, session: nil,
email_token: nil, email_token: nil,
redeeming_user: nil) redeeming_user: nil)
@invite = invite @invite = invite
@email = email
@username = username @username = username
@name = name @name = name
@password = password @password = password
@ -34,6 +45,8 @@ class InviteRedeemer
@session = session @session = session
@email_token = email_token @email_token = email_token
@redeeming_user = redeeming_user @redeeming_user = redeeming_user
ensure_email_is_present!(email)
end end
def redeem def redeem
@ -45,7 +58,29 @@ class InviteRedeemer
end end
end end
# extracted from User cause it is very specific to invites # The email must be present in some form since many of the methods
# for processing + redemption rely on it. If it's still nil after
# these checks then we have hit an edge case and should not proceed!
def ensure_email_is_present!(email)
if email.blank?
Rails.logger.warn(
"email param was blank in InviteRedeemer for invite ID #{@invite.id}. The `redeeming_user` was #{@redeeming_user.present? ? "(ID: #{@redeeming_user.id})" : "not"} present.",
)
end
if email.blank? && @invite.is_email_invite?
@email = @invite.email
elsif @redeeming_user.present?
@email = @redeeming_user.email
else
@email = email
end
raise Discourse::InvalidParameters if @email.blank?
end
# This will _never_ be called if there is a redeeming_user being passed
# in to InviteRedeemer -- see invited_user below.
def self.create_user_from_invite(email:, invite:, username: nil, name: nil, password: nil, user_custom_fields: nil, ip_address: nil, session: nil, email_token: nil) def self.create_user_from_invite(email:, invite:, username: nil, name: nil, password: nil, user_custom_fields: nil, ip_address: nil, session: nil, email_token: nil)
if username && UsernameValidator.new(username).valid_format? && User.username_available?(username, email) if username && UsernameValidator.new(username).valid_format? && User.username_available?(username, email)
available_username = username available_username = username
@ -107,7 +142,10 @@ class InviteRedeemer
user.save! user.save!
authenticator.finish authenticator.finish
if invite.emailed_status != Invite.emailed_status_types[:not_required] && email == invite.email && invite.email_token.present? && email_token == invite.email_token if invite.emailed_status != Invite.emailed_status_types[:not_required] &&
email == invite.email &&
invite.email_token.present? &&
email_token == invite.email_token
user.activate user.activate
end end
@ -118,24 +156,26 @@ class InviteRedeemer
def can_redeem_invite? def can_redeem_invite?
return false if !invite.redeemable? return false if !invite.redeemable?
return false if email.blank?
# Invite has already been redeemed by anyone. # Invite scoped to email has already been redeemed by anyone.
if !invite.is_invite_link? && InvitedUser.exists?(invite_id: invite.id) if invite.is_email_invite? && InvitedUser.exists?(invite_id: invite.id)
return false return false
end end
# Email will not be present if we are claiming an invite link, which # The email will be present for either an invite link (where the user provides
# does not have an email or domain scope on the invitation. # us the email manually) or for an invite scoped to an email, where we
if email.present? || redeeming_user.present? # prefill the email and do not let the user modify it.
email_to_check = redeeming_user&.email || email #
# Note that an invite link can also have a domain scope which must be checked.
email_to_check = redeeming_user&.email || email
if invite.email.present? && !invite.email_matches?(email_to_check) if invite.email.present? && !invite.email_matches?(email_to_check)
raise ActiveRecord::RecordNotSaved.new(I18n.t('invite.not_matching_email')) raise ActiveRecord::RecordNotSaved.new(I18n.t('invite.not_matching_email'))
end end
if invite.domain.present? && !invite.domain_matches?(email_to_check) if invite.domain.present? && !invite.domain_matches?(email_to_check)
raise ActiveRecord::RecordNotSaved.new(I18n.t('invite.domain_not_allowed')) raise ActiveRecord::RecordNotSaved.new(I18n.t('invite.domain_not_allowed'))
end
end end
# Anon user is trying to redeem an invitation, if an existing user already # Anon user is trying to redeem an invitation, if an existing user already
@ -148,6 +188,10 @@ class InviteRedeemer
true true
end end
# Note that the invited_user is returned by #redeemed, so other places
# (e.g. the InvitesController) can perform further actions on it, this
# is why things like send_welcome_message are set without being saved
# on the model.
def invited_user def invited_user
return @invited_user if defined?(@invited_user) return @invited_user if defined?(@invited_user)
@ -196,9 +240,18 @@ class InviteRedeemer
end end
def add_to_private_topics_if_invited def add_to_private_topics_if_invited
topic_ids = Topic.where(archetype: Archetype::private_message).includes(:invites).where(invites: { email: email }).pluck(:id) # Should not happen because of ensure_email_is_present!, but better to cover bases.
return if email.blank?
topic_ids = TopicInvite.joins(:invite)
.joins(:topic)
.where("topics.archetype = ?", Archetype::private_message)
.where("invites.email = ?", email)
.pluck(:topic_id)
topic_ids.each do |id| topic_ids.each do |id|
TopicAllowedUser.create!(user_id: invited_user.id, topic_id: id) unless TopicAllowedUser.exists?(user_id: invited_user.id, topic_id: id) if !TopicAllowedUser.exists?(user_id: invited_user.id, topic_id: id)
TopicAllowedUser.create!(user_id: invited_user.id, topic_id: id)
end
end end
end end
@ -221,15 +274,17 @@ class InviteRedeemer
end end
def notify_invitee def notify_invitee
if inviter = invite.invited_by return if invite.invited_by.blank?
inviter.notifications.create!( invite.invited_by.notifications.create!(
notification_type: Notification.types[:invitee_accepted], notification_type: Notification.types[:invitee_accepted],
data: { display_username: invited_user.username }.to_json data: { display_username: invited_user.username }.to_json
) )
end
end end
def delete_duplicate_invites def delete_duplicate_invites
# Should not happen because of ensure_email_is_present!, but better to cover bases.
return if email.blank?
Invite Invite
.where('invites.max_redemptions_allowed = 1') .where('invites.max_redemptions_allowed = 1')
.joins("LEFT JOIN invited_users ON invites.id = invited_users.invite_id") .joins("LEFT JOIN invited_users ON invites.id = invited_users.invite_id")

View File

@ -0,0 +1,60 @@
# frozen_string_literal: true
class RemoveInvalidTopicAllowedUsersFromInvites < ActiveRecord::Migration[7.0]
def up
# We are getting all the topic_allowed_users records that
# match an invited user, which is created as part of the invite
# redemption flow. The original invite would _not_ have had a topic_invite
# record, and the user should have been added to the topic in the brief
# period between creation of the invited_users record and the update of
# that record.
#
# Having > 2 topic allowed users disqualifies messages sent only
# by the system or an admin to the user.
subquery_sql = <<~SQL
SELECT DISTINCT id
FROM (
SELECT tau.id, tau.user_id, COUNT(*) OVER (PARTITION BY tau.user_id)
FROM topic_allowed_users tau
JOIN invited_users iu ON iu.user_id = tau.user_id
LEFT JOIN topic_invites ti ON ti.invite_id = iu.invite_id AND tau.topic_id = ti.topic_id
WHERE ti.id IS NULL
AND tau.created_at BETWEEN iu.created_at AND iu.updated_at
AND iu.redeemed_at > '2022-10-27'
) AS matching_topic_allowed_users
WHERE matching_topic_allowed_users.count > 2
SQL
# Back up the records we are going to change in case we are too
# brutal, and for further inspection.
#
# TODO DROP this table (topic_allowed_users_backup_nov_2022) in a later migration.
DB.exec(<<~SQL)
CREATE TABLE topic_allowed_users_backup_nov_2022
(
id INT NOT NULL,
user_id INT NOT NULL,
topic_id INT NOT NULL
);
INSERT INTO topic_allowed_users_backup_nov_2022(id, user_id, topic_id)
SELECT id, user_id, topic_id
FROM topic_allowed_users
WHERE id IN (
#{subquery_sql}
)
SQL
# Delete the invalid topic allowed users that should not be there.
DB.query(<<~SQL)
DELETE
FROM topic_allowed_users
WHERE id IN (
#{subquery_sql}
)
SQL
end
def down
raise ActiveRecord::IrreversibleMigration
end
end

View File

@ -3,6 +3,83 @@
RSpec.describe InviteRedeemer do RSpec.describe InviteRedeemer do
fab!(:admin) { Fabricate(:admin) } fab!(:admin) { Fabricate(:admin) }
describe "#initialize" do
fab!(:redeeming_user) { Fabricate(:user, email: "redeemer@test.com") }
context "for invite link" do
fab!(:invite) { Fabricate(:invite, email: nil) }
context "when an email is passed in without a redeeming user" do
it "uses that email for invite redemption" do
redeemer = described_class.new(invite: invite, email: "blah@test.com")
expect(redeemer.email).to eq("blah@test.com")
expect { redeemer.redeem }.to change { User.count }
expect(User.find_by_email(redeemer.email)).to be_present
end
end
context "when an email is passed in with a redeeming user" do
it "uses the redeeming user's email for invite redemption" do
redeemer = described_class.new(invite: invite, email: "blah@test.com", redeeming_user: redeeming_user)
expect(redeemer.email).to eq(redeeming_user.email)
expect { redeemer.redeem }.not_to change { User.count }
end
end
context "when an email is not passed in with a redeeming user" do
it "uses the redeeming user's email for invite redemption" do
redeemer = described_class.new(invite: invite, email: nil, redeeming_user: redeeming_user)
expect(redeemer.email).to eq(redeeming_user.email)
expect { redeemer.redeem }.not_to change { User.count }
end
end
context "when no email and no redeeming user is passed in" do
it "raises an error" do
expect { described_class.new(invite: invite, email: nil, redeeming_user: nil) }.to raise_error(Discourse::InvalidParameters)
end
end
end
context "for invite with email" do
fab!(:invite) { Fabricate(:invite, email: "foobar@example.com") }
context "when an email is passed in without a redeeming user" do
it "uses that email for invite redemption" do
redeemer = described_class.new(invite: invite, email: "foobar@example.com")
expect(redeemer.email).to eq("foobar@example.com")
expect { redeemer.redeem }.to change { User.count }
expect(User.find_by_email(redeemer.email)).to be_present
end
end
context "when an email is passed in with a redeeming user" do
it "uses the redeeming user's email for invite redemption" do
redeemer = described_class.new(invite: invite, email: "blah@test.com", redeeming_user: redeeming_user)
expect(redeemer.email).to eq(redeeming_user.email)
expect { redeemer.redeem }.to raise_error(ActiveRecord::RecordNotSaved, I18n.t("invite.not_matching_email"))
end
end
context "when an email is not passed in with a redeeming user" do
it "uses the invite email for invite redemption" do
redeemer = described_class.new(invite: invite, email: nil, redeeming_user: redeeming_user)
expect(redeemer.email).to eq("foobar@example.com")
expect { redeemer.redeem }.to raise_error(ActiveRecord::RecordNotSaved, I18n.t("invite.not_matching_email"))
end
end
context "when no email and no redeeming user is passed in" do
it "uses the invite email for invite redemption" do
redeemer = described_class.new(invite: invite, email: nil, redeeming_user: nil)
expect(redeemer.email).to eq("foobar@example.com")
expect { redeemer.redeem }.to change { User.count }
expect(User.find_by_email(redeemer.email)).to be_present
end
end
end
end
describe '.create_user_from_invite' do describe '.create_user_from_invite' do
it "should be created correctly" do it "should be created correctly" do
invite = Fabricate(:invite, email: 'walter.white@email.com') invite = Fabricate(:invite, email: 'walter.white@email.com')
@ -113,172 +190,199 @@ RSpec.describe InviteRedeemer do
end end
describe "#redeem" do describe "#redeem" do
fab!(:invite) { Fabricate(:invite, email: "foobar@example.com") }
let(:name) { 'john snow' } let(:name) { 'john snow' }
let(:username) { 'kingofthenorth' } let(:username) { 'kingofthenorth' }
let(:password) { 'know5nOthiNG' } let(:password) { 'know5nOthiNG' }
let(:invite_redeemer) { InviteRedeemer.new(invite: invite, email: invite.email, username: username, name: name) } let(:invite_redeemer) { InviteRedeemer.new(invite: invite, email: invite.email, username: username, name: name) }
context "when must_approve_users setting is enabled" do context "with email" do
before do fab!(:invite) { Fabricate(:invite, email: "foobar@example.com") }
SiteSetting.must_approve_users = true context "when must_approve_users setting is enabled" do
before do
SiteSetting.must_approve_users = true
end
it "should redeem an invite but not approve the user when invite is created by a staff user" do
inviter = invite.invited_by
inviter.update!(admin: true)
user = invite_redeemer.redeem
expect(user.name).to eq(name)
expect(user.username).to eq(username)
expect(user.invited_by).to eq(inviter)
expect(user.approved).to eq(false)
expect(inviter.notifications.count).to eq(1)
end
it "should redeem the invite but not approve the user when invite is created by a regular user" do
inviter = invite.invited_by
user = invite_redeemer.redeem
expect(user.name).to eq(name)
expect(user.username).to eq(username)
expect(user.invited_by).to eq(inviter)
expect(user.approved).to eq(false)
expect(inviter.notifications.count).to eq(1)
end
it "should redeem the invite and approve the user when user email is in auto_approve_email_domains setting" do
SiteSetting.auto_approve_email_domains = "example.com"
user = invite_redeemer.redeem
expect(user.name).to eq(name)
expect(user.username).to eq(username)
expect(user.approved).to eq(true)
expect(user.approved_by).to eq(Discourse.system_user)
end
end end
it "should redeem an invite but not approve the user when invite is created by a staff user" do it "should redeem the invite if invited by non staff and approve if staff not required to approve" do
inviter = invite.invited_by
inviter.update!(admin: true)
user = invite_redeemer.redeem
expect(user.name).to eq(name)
expect(user.username).to eq(username)
expect(user.invited_by).to eq(inviter)
expect(user.approved).to eq(false)
expect(inviter.notifications.count).to eq(1)
end
it "should redeem the invite but not approve the user when invite is created by a regular user" do
inviter = invite.invited_by inviter = invite.invited_by
user = invite_redeemer.redeem user = invite_redeemer.redeem
expect(user.name).to eq(name) expect(user.name).to eq(name)
expect(user.username).to eq(username) expect(user.username).to eq(username)
expect(user.invited_by).to eq(inviter) expect(user.invited_by).to eq(inviter)
expect(user.approved).to eq(false)
expect(inviter.notifications.count).to eq(1) expect(inviter.notifications.count).to eq(1)
expect(user.approved).to eq(false)
end end
it "should redeem the invite and approve the user when user email is in auto_approve_email_domains setting" do it "should delete invite if invited_by user has been removed" do
SiteSetting.auto_approve_email_domains = "example.com" invite.invited_by.destroy!
expect { invite.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
it "can set password" do
user = InviteRedeemer.new(invite: invite, email: invite.email, username: username, name: name, password: password).redeem
expect(user).to have_password
expect(user.confirm_password?(password)).to eq(true)
expect(user.approved).to eq(false)
end
it "can set custom fields" do
required_field = Fabricate(:user_field)
optional_field = Fabricate(:user_field, required: false)
user_fields = {
required_field.id.to_s => 'value1',
optional_field.id.to_s => 'value2'
}
user = InviteRedeemer.new(invite: invite, email: invite.email, username: username, name: name, password: password, user_custom_fields: user_fields).redeem
expect(user).to be_present
expect(user.custom_fields["user_field_#{required_field.id}"]).to eq('value1')
expect(user.custom_fields["user_field_#{optional_field.id}"]).to eq('value2')
end
it "does not add user to group if inviter does not have permissions" do
group = Fabricate(:group, grant_trust_level: 2)
InvitedGroup.create(group_id: group.id, invite_id: invite.id)
user = InviteRedeemer.new(invite: invite, email: invite.email, username: username, name: name, password: password).redeem
expect(user.group_users.count).to eq(0)
end
it "adds user to group" do
group = Fabricate(:group, grant_trust_level: 2)
InvitedGroup.create(group_id: group.id, invite_id: invite.id)
group.add_owner(invite.invited_by)
user = InviteRedeemer.new(invite: invite, email: invite.email, username: username, name: name, password: password).redeem
expect(user.group_users.count).to eq(4)
expect(user.trust_level).to eq(2)
end
it "adds an entry to the group logs when the invited user is added to a group" do
group = Fabricate(:group)
InvitedGroup.create(group_id: group.id, invite_id: invite.id)
group.add_owner(invite.invited_by)
GroupHistory.destroy_all
user = InviteRedeemer.new(
invite: invite,
email: invite.email,
username: username,
name: name,
password: password
).redeem
expect(group.reload.usernames.split(",")).to include(user.username)
expect(GroupHistory.exists?(
target_user_id: user.id,
acting_user: invite.invited_by.id,
group_id: group.id,
action: GroupHistory.actions[:add_user_to_group]
)).to eq(true)
end
it "only allows one user to be created per invite" do
user = invite_redeemer.redeem user = invite_redeemer.redeem
invite.reload
expect(user.name).to eq(name) user.email = "john@example.com"
expect(user.username).to eq(username) user.save!
expect(user.approved).to eq(true)
expect(user.approved_by).to eq(Discourse.system_user) another_invite_redeemer = InviteRedeemer.new(invite: invite, email: invite.email, username: username, name: name)
another_user = another_invite_redeemer.redeem
expect(another_user).to eq(nil)
end end
end
it "should redeem the invite if invited by non staff and approve if staff not required to approve" do it "should correctly update the invite redeemed_at date" do
inviter = invite.invited_by SiteSetting.invite_expiry_days = 2
user = invite_redeemer.redeem invite.update!(created_at: 10.days.ago)
expect(user.name).to eq(name) inviter = invite.invited_by
expect(user.username).to eq(username) inviter.admin = true
expect(user.invited_by).to eq(inviter) user = invite_redeemer.redeem
expect(inviter.notifications.count).to eq(1) invite.reload
expect(user.approved).to eq(false)
end
it "should delete invite if invited_by user has been removed" do expect(user.invited_by).to eq(inviter)
invite.invited_by.destroy! expect(inviter.notifications.count).to eq(1)
expect { invite.reload }.to raise_error(ActiveRecord::RecordNotFound) expect(invite.invited_users.first).to be_present
end end
it "can set password" do
user = InviteRedeemer.new(invite: invite, email: invite.email, username: username, name: name, password: password).redeem
expect(user).to have_password
expect(user.confirm_password?(password)).to eq(true)
expect(user.approved).to eq(false)
end
it "can set custom fields" do
required_field = Fabricate(:user_field)
optional_field = Fabricate(:user_field, required: false)
user_fields = {
required_field.id.to_s => 'value1',
optional_field.id.to_s => 'value2'
}
user = InviteRedeemer.new(invite: invite, email: invite.email, username: username, name: name, password: password, user_custom_fields: user_fields).redeem
expect(user).to be_present
expect(user.custom_fields["user_field_#{required_field.id}"]).to eq('value1')
expect(user.custom_fields["user_field_#{optional_field.id}"]).to eq('value2')
end
it "does not add user to group if inviter does not have permissions" do
group = Fabricate(:group, grant_trust_level: 2)
InvitedGroup.create(group_id: group.id, invite_id: invite.id)
user = InviteRedeemer.new(invite: invite, email: invite.email, username: username, name: name, password: password).redeem
expect(user.group_users.count).to eq(0)
end
it "adds user to group" do
group = Fabricate(:group, grant_trust_level: 2)
InvitedGroup.create(group_id: group.id, invite_id: invite.id)
group.add_owner(invite.invited_by)
user = InviteRedeemer.new(invite: invite, email: invite.email, username: username, name: name, password: password).redeem
expect(user.group_users.count).to eq(4)
expect(user.trust_level).to eq(2)
end
it "adds an entry to the group logs when the invited user is added to a group" do
group = Fabricate(:group)
InvitedGroup.create(group_id: group.id, invite_id: invite.id)
group.add_owner(invite.invited_by)
GroupHistory.destroy_all
user = InviteRedeemer.new(
invite: invite,
email: invite.email,
username: username,
name: name,
password: password
).redeem
expect(group.reload.usernames.split(",")).to include(user.username)
expect(GroupHistory.exists?(
target_user_id: user.id,
acting_user: invite.invited_by.id,
group_id: group.id,
action: GroupHistory.actions[:add_user_to_group]
)).to eq(true)
end
it "only allows one user to be created per invite" do
user = invite_redeemer.redeem
invite.reload
user.email = "john@example.com"
user.save!
another_invite_redeemer = InviteRedeemer.new(invite: invite, email: invite.email, username: username, name: name)
another_user = another_invite_redeemer.redeem
expect(another_user).to eq(nil)
end
it "should correctly update the invite redeemed_at date" do
SiteSetting.invite_expiry_days = 2
invite.update!(created_at: 10.days.ago)
inviter = invite.invited_by
inviter.admin = true
user = invite_redeemer.redeem
invite.reload
expect(user.invited_by).to eq(inviter)
expect(inviter.notifications.count).to eq(1)
expect(invite.invited_users.first).to be_present
end
it "raises an error if the email does not match the invite email" do
redeemer = InviteRedeemer.new(invite: invite, email: "blah@test.com", username: username, name: name)
expect { redeemer.redeem }.to raise_error(ActiveRecord::RecordNotSaved, I18n.t("invite.not_matching_email"))
end
context "when a redeeming user is passed in" do
fab!(:redeeming_user) { Fabricate(:user, email: "foobar@example.com") }
it "raises an error if the email does not match the invite email" do it "raises an error if the email does not match the invite email" do
redeeming_user.update!(email: "foo@bar.com") redeemer = InviteRedeemer.new(invite: invite, email: "blah@test.com", username: username, name: name)
redeemer = InviteRedeemer.new(invite: invite, redeeming_user: redeeming_user)
expect { redeemer.redeem }.to raise_error(ActiveRecord::RecordNotSaved, I18n.t("invite.not_matching_email")) expect { redeemer.redeem }.to raise_error(ActiveRecord::RecordNotSaved, I18n.t("invite.not_matching_email"))
end end
it "adds the user to the appropriate private topic and no others" do
topic1 = Fabricate(:private_message_topic)
topic2 = Fabricate(:private_message_topic)
TopicInvite.create(invite: invite, topic: topic1)
user = InviteRedeemer.new(invite: invite, email: invite.email, username: username, name: name, password: password).redeem
expect(TopicAllowedUser.exists?(topic: topic1, user: user)).to eq(true)
expect(TopicAllowedUser.exists?(topic: topic2, user: user)).to eq(false)
end
context "when a redeeming user is passed in" do
fab!(:redeeming_user) { Fabricate(:user, email: "foobar@example.com") }
it "raises an error if the email does not match the invite email" do
redeeming_user.update!(email: "foo@bar.com")
redeemer = InviteRedeemer.new(invite: invite, redeeming_user: redeeming_user)
expect { redeemer.redeem }.to raise_error(ActiveRecord::RecordNotSaved, I18n.t("invite.not_matching_email"))
end
it "adds the user to the appropriate private topic and no others" do
topic1 = Fabricate(:private_message_topic)
topic2 = Fabricate(:private_message_topic)
TopicInvite.create(invite: invite, topic: topic1)
InviteRedeemer.new(invite: invite, redeeming_user: redeeming_user).redeem
expect(TopicAllowedUser.exists?(topic: topic1, user: redeeming_user)).to eq(true)
expect(TopicAllowedUser.exists?(topic: topic2, user: redeeming_user)).to eq(false)
end
it "does not create a topic allowed user record if the invited user is already in the topic" do
topic1 = Fabricate(:private_message_topic)
TopicInvite.create(invite: invite, topic: topic1)
TopicAllowedUser.create(topic: topic1, user: redeeming_user)
expect { InviteRedeemer.new(invite: invite, redeeming_user: redeeming_user).redeem }.not_to change { TopicAllowedUser.count }
end
end
end end
context 'with domain' do context 'with domain' do

View File

@ -912,76 +912,144 @@ RSpec.describe InvitesController do
end end
context 'when user is already logged in' do context 'when user is already logged in' do
fab!(:invite) { Fabricate(:invite, email: 'test@example.com') }
fab!(:user) { Fabricate(:user, email: 'test@example.com') }
fab!(:group) { Fabricate(:group) }
before { sign_in(user) } before { sign_in(user) }
it 'redeems the invitation and creates the invite accepted notification' do context "for an email invite" do
put "/invites/show/#{invite.invite_key}.json", params: { id: invite.invite_key } fab!(:invite) { Fabricate(:invite, email: 'test@example.com') }
expect(response.status).to eq(200) fab!(:user) { Fabricate(:user, email: 'test@example.com') }
expect(response.parsed_body["message"]).to eq(I18n.t("invite.existing_user_success")) fab!(:group) { Fabricate(:group) }
invite.reload
expect(invite.invited_users.first.user).to eq(user)
expect(invite.redeemed?).to be_truthy
expect(
Notification.exists?(
user: invite.invited_by, notification_type: Notification.types[:invitee_accepted]
)
).to eq(true)
end
it 'redirects to the first topic the user was invited to and creates the topic notification' do it 'redeems the invitation and creates the invite accepted notification' do
topic = Fabricate(:topic)
TopicInvite.create!(invite: invite, topic: topic)
put "/invites/show/#{invite.invite_key}.json", params: { id: invite.invite_key }
expect(response.status).to eq(200)
expect(response.parsed_body['redirect_to']).to eq(topic.relative_url)
expect(Notification.where(notification_type: Notification.types[:invited_to_topic], topic: topic).count).to eq(1)
end
it "adds the user to the groups specified on the invite and allows them to access the secure topic" do
group.add_owner(invite.invited_by)
secured_category = Fabricate(:category)
secured_category.permissions = { group.name => :full }
secured_category.save!
topic = Fabricate(:topic, category: secured_category)
TopicInvite.create!(invite: invite, topic: topic)
InvitedGroup.create!(invite: invite, group: group)
put "/invites/show/#{invite.invite_key}.json", params: { id: invite.invite_key }
expect(response.status).to eq(200)
expect(response.parsed_body["message"]).to eq(I18n.t("invite.existing_user_success"))
expect(response.parsed_body['redirect_to']).to eq(topic.relative_url)
invite.reload
expect(invite.redeemed?).to be_truthy
expect(user.reload.groups).to include(group)
expect(Notification.where(notification_type: Notification.types[:invited_to_topic], topic: topic).count).to eq(1)
end
it "does not try to log in the user automatically" do
expect do
put "/invites/show/#{invite.invite_key}.json", params: { id: invite.invite_key } put "/invites/show/#{invite.invite_key}.json", params: { id: invite.invite_key }
end.not_to change { UserAuthToken.count } expect(response.status).to eq(200)
expect(response.status).to eq(200) expect(response.parsed_body["message"]).to eq(I18n.t("invite.existing_user_success"))
expect(response.parsed_body["message"]).to eq(I18n.t("invite.existing_user_success")) invite.reload
expect(invite.invited_users.first.user).to eq(user)
expect(invite.redeemed?).to be_truthy
expect(
Notification.exists?(
user: invite.invited_by, notification_type: Notification.types[:invitee_accepted]
)
).to eq(true)
end
it 'redirects to the first topic the user was invited to and creates the topic notification' do
topic = Fabricate(:topic)
TopicInvite.create!(invite: invite, topic: topic)
put "/invites/show/#{invite.invite_key}.json", params: { id: invite.invite_key }
expect(response.status).to eq(200)
expect(response.parsed_body['redirect_to']).to eq(topic.relative_url)
expect(Notification.where(notification_type: Notification.types[:invited_to_topic], topic: topic).count).to eq(1)
end
it "adds the user to the private topic" do
topic = Fabricate(:private_message_topic)
TopicInvite.create!(invite: invite, topic: topic)
put "/invites/show/#{invite.invite_key}.json", params: { id: invite.invite_key }
expect(response.status).to eq(200)
expect(response.parsed_body['redirect_to']).to eq(topic.relative_url)
expect(TopicAllowedUser.exists?(user: user, topic: topic)).to eq(true)
end
it "adds the user to the groups specified on the invite and allows them to access the secure topic" do
group.add_owner(invite.invited_by)
secured_category = Fabricate(:category)
secured_category.permissions = { group.name => :full }
secured_category.save!
topic = Fabricate(:topic, category: secured_category)
TopicInvite.create!(invite: invite, topic: topic)
InvitedGroup.create!(invite: invite, group: group)
put "/invites/show/#{invite.invite_key}.json", params: { id: invite.invite_key }
expect(response.status).to eq(200)
expect(response.parsed_body["message"]).to eq(I18n.t("invite.existing_user_success"))
expect(response.parsed_body['redirect_to']).to eq(topic.relative_url)
invite.reload
expect(invite.redeemed?).to be_truthy
expect(user.reload.groups).to include(group)
expect(Notification.where(notification_type: Notification.types[:invited_to_topic], topic: topic).count).to eq(1)
end
it "does not try to log in the user automatically" do
expect do
put "/invites/show/#{invite.invite_key}.json", params: { id: invite.invite_key }
end.not_to change { UserAuthToken.count }
expect(response.status).to eq(200)
expect(response.parsed_body["message"]).to eq(I18n.t("invite.existing_user_success"))
end
it "errors if the user's email doesn't match the invite email" do
user.update!(email: "blah@test.com")
put "/invites/show/#{invite.invite_key}.json", params: { id: invite.invite_key }
expect(response.status).to eq(412)
expect(response.parsed_body["message"]).to eq(I18n.t("invite.not_matching_email"))
end
it "errors if the user's email domain doesn't match the invite domain" do
user.update!(email: "blah@test.com")
invite.update!(email: nil, domain: "example.com")
put "/invites/show/#{invite.invite_key}.json", params: { id: invite.invite_key }
expect(response.status).to eq(412)
expect(response.parsed_body["message"]).to eq(I18n.t("invite.domain_not_allowed"))
end
end end
it "errors if the user's email doesn't match the invite email" do context "for an invite link" do
user.update!(email: "blah@test.com") fab!(:invite) { Fabricate(:invite, email: nil) }
put "/invites/show/#{invite.invite_key}.json", params: { id: invite.invite_key } fab!(:user) { Fabricate(:user, email: 'test@example.com') }
expect(response.status).to eq(412) fab!(:group) { Fabricate(:group) }
expect(response.parsed_body["message"]).to eq(I18n.t("invite.not_matching_email"))
end
it "errors if the user's email domain doesn't match the invite domain" do it 'redeems the invitation and creates the invite accepted notification' do
user.update!(email: "blah@test.com") put "/invites/show/#{invite.invite_key}.json", params: { id: invite.invite_key }
invite.update!(email: nil, domain: "example.com") expect(response.status).to eq(200)
put "/invites/show/#{invite.invite_key}.json", params: { id: invite.invite_key } expect(response.parsed_body["message"]).to eq(I18n.t("invite.existing_user_success"))
expect(response.status).to eq(412) invite.reload
expect(response.parsed_body["message"]).to eq(I18n.t("invite.domain_not_allowed")) expect(invite.invited_users.first.user).to eq(user)
expect(invite.redeemed?).to be_truthy
expect(
Notification.exists?(
user: invite.invited_by, notification_type: Notification.types[:invitee_accepted]
)
).to eq(true)
end
it 'redirects to the first topic the user was invited to and creates the topic notification' do
topic = Fabricate(:topic)
TopicInvite.create!(invite: invite, topic: topic)
put "/invites/show/#{invite.invite_key}.json", params: { id: invite.invite_key }
expect(response.status).to eq(200)
expect(response.parsed_body['redirect_to']).to eq(topic.relative_url)
expect(Notification.where(notification_type: Notification.types[:invited_to_topic], topic: topic).count).to eq(1)
end
it "adds the user to the groups specified on the invite and allows them to access the secure topic" do
group.add_owner(invite.invited_by)
secured_category = Fabricate(:category)
secured_category.permissions = { group.name => :full }
secured_category.save!
topic = Fabricate(:topic, category: secured_category)
TopicInvite.create!(invite: invite, topic: topic)
InvitedGroup.create!(invite: invite, group: group)
put "/invites/show/#{invite.invite_key}.json", params: { id: invite.invite_key }
expect(response.status).to eq(200)
expect(response.parsed_body["message"]).to eq(I18n.t("invite.existing_user_success"))
expect(response.parsed_body['redirect_to']).to eq(topic.relative_url)
invite.reload
expect(invite.redeemed?).to be_truthy
expect(user.reload.groups).to include(group)
expect(Notification.where(notification_type: Notification.types[:invited_to_topic], topic: topic).count).to eq(1)
end
it "does not try to log in the user automatically" do
expect do
put "/invites/show/#{invite.invite_key}.json", params: { id: invite.invite_key }
end.not_to change { UserAuthToken.count }
expect(response.status).to eq(200)
expect(response.parsed_body["message"]).to eq(I18n.t("invite.existing_user_success"))
end
end end
end end