FIX: allow existing users to be invited to topic/message when must_approve_users is enabled

This commit is contained in:
Arpit Jalan 2017-02-02 23:08:25 +05:30
parent 06570f8e5a
commit dc2171960b
9 changed files with 113 additions and 81 deletions

View File

@ -27,19 +27,22 @@ export default Ember.Controller.extend(ModalFunctionality, {
return Discourse.User.currentProp("admin"); return Discourse.User.currentProp("admin");
}.property(), }.property(),
disabled: function() { @computed('isAdmin', 'emailOrUsername', 'invitingToTopic', 'isPrivateTopic', 'model.groupNames', 'model.saving', 'model.details.can_invite_to')
if (this.get('model.saving')) return true; disabled(isAdmin, emailOrUsername, invitingToTopic, isPrivateTopic, groupNames, saving, can_invite_to) {
if (Ember.isEmpty(this.get('emailOrUsername'))) return true; if (saving) return true;
const emailOrUsername = this.get('emailOrUsername').trim(); if (Ember.isEmpty(emailOrUsername)) return true;
const emailTrimmed = emailOrUsername.trim();
// when inviting to forum, email must be valid // when inviting to forum, email must be valid
if (!this.get('invitingToTopic') && !emailValid(emailOrUsername)) return true; if (!invitingToTopic && !emailValid(emailTrimmed)) return true;
// normal users (not admin) can't invite users to private topic via email // normal users (not admin) can't invite users to private topic via email
if (!this.get('isAdmin') && this.get('isPrivateTopic') && emailValid(emailOrUsername)) return true; if (!isAdmin && isPrivateTopic && emailValid(emailTrimmed)) return true;
// when inviting to private topic via email, group name must be specified // when inviting to private topic via email, group name must be specified
if (this.get('isPrivateTopic') && Ember.isEmpty(this.get('model.groupNames')) && emailValid(emailOrUsername)) return true; if (isPrivateTopic && Ember.isEmpty(groupNames) && emailValid(emailTrimmed)) return true;
if (this.get('model.details.can_invite_to')) return false;
if (can_invite_to) return false;
return false; return false;
}.property('isAdmin', 'emailOrUsername', 'invitingToTopic', 'isPrivateTopic', 'model.groupNames', 'model.saving'), },
disabledCopyLink: function() { disabledCopyLink: function() {
if (this.get('hasCustomMessage')) return true; if (this.get('hasCustomMessage')) return true;
@ -65,9 +68,10 @@ export default Ember.Controller.extend(ModalFunctionality, {
return this.get('model') !== this.currentUser; return this.get('model') !== this.currentUser;
}.property('model'), }.property('model'),
showCopyInviteButton: function() { @computed('isMessage', 'model.details.can_invite_via_email')
return (!Discourse.SiteSettings.enable_sso && !this.get('isMessage')); showCopyInviteButton(isMessage, can_invite_via_email) {
}.property('isMessage'), return (can_invite_via_email && !isMessage);
},
topicId: Ember.computed.alias('model.id'), topicId: Ember.computed.alias('model.id'),
@ -83,32 +87,38 @@ export default Ember.Controller.extend(ModalFunctionality, {
}.property('invitingToTopic'), }.property('invitingToTopic'),
// Show Groups? (add invited user to private group) // Show Groups? (add invited user to private group)
showGroups: function() { @computed('isAdmin', 'emailOrUsername', 'isPrivateTopic', 'isMessage', 'invitingToTopic', 'model.details.can_invite_via_email')
return this.get('isAdmin') && (emailValid(this.get('emailOrUsername')) || this.get('isPrivateTopic') || !this.get('invitingToTopic')) && !Discourse.SiteSettings.enable_sso && Discourse.SiteSettings.enable_local_logins && !this.get('isMessage'); showGroups(isAdmin, emailOrUsername, isPrivateTopic, isMessage, invitingToTopic, can_invite_via_email) {
}.property('isAdmin', 'emailOrUsername', 'isPrivateTopic', 'isMessage', 'invitingToTopic'), return isAdmin &&
can_invite_via_email &&
!isMessage &&
(emailValid(emailOrUsername) || isPrivateTopic || !invitingToTopic);
},
showCustomMessage: function() { @computed('emailOrUsername')
return (this.get('model') === this.currentUser || emailValid(this.get('emailOrUsername'))); showCustomMessage(emailOrUsername) {
}.property('emailOrUsername'), return (this.get('model') === this.currentUser || emailValid(emailOrUsername));
},
// Instructional text for the modal. // Instructional text for the modal.
inviteInstructions: function() { @computed('isMessage', 'invitingToTopic', 'emailOrUsername', 'isPrivateTopic', 'isAdmin', 'model.details.can_invite_via_email')
if (Discourse.SiteSettings.enable_sso || !Discourse.SiteSettings.enable_local_logins) { inviteInstructions(isMessage, invitingToTopic, emailOrUsername, isPrivateTopic, isAdmin, can_invite_via_email) {
// inviting existing user when SSO enabled if (!can_invite_via_email) {
// can't invite via email, only existing users
return I18n.t('topic.invite_reply.sso_enabled'); return I18n.t('topic.invite_reply.sso_enabled');
} else if (this.get('isMessage')) { } else if (isMessage) {
// inviting to a message // inviting to a message
return I18n.t('topic.invite_private.email_or_username'); return I18n.t('topic.invite_private.email_or_username');
} else if (this.get('invitingToTopic')) { } else if (invitingToTopic) {
// inviting to a private/public topic // inviting to a private/public topic
if (this.get('isPrivateTopic') && !this.get('isAdmin')) { if (isPrivateTopic && !isAdmin) {
// inviting to a private topic and is not admin // inviting to a private topic and is not admin
return I18n.t('topic.invite_reply.to_username'); return I18n.t('topic.invite_reply.to_username');
} else { } else {
// when inviting to a topic, display instructions based on provided entity // when inviting to a topic, display instructions based on provided entity
if (Ember.isEmpty(this.get('emailOrUsername'))) { if (Ember.isEmpty(emailOrUsername)) {
return I18n.t('topic.invite_reply.to_topic_blank'); return I18n.t('topic.invite_reply.to_topic_blank');
} else if (emailValid(this.get('emailOrUsername'))) { } else if (emailValid(emailOrUsername)) {
this.set("inviteIcon", "envelope"); this.set("inviteIcon", "envelope");
return I18n.t('topic.invite_reply.to_topic_email'); return I18n.t('topic.invite_reply.to_topic_email');
} else { } else {
@ -120,7 +130,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
// inviting to forum // inviting to forum
return I18n.t('topic.invite_reply.to_forum'); return I18n.t('topic.invite_reply.to_forum');
} }
}.property('isMessage', 'invitingToTopic', 'emailOrUsername'), },
showGroupsClass: function() { showGroupsClass: function() {
return this.get('isPrivateTopic') ? 'required' : 'optional'; return this.get('isPrivateTopic') ? 'required' : 'optional';
@ -147,11 +157,12 @@ export default Ember.Controller.extend(ModalFunctionality, {
return this.get('isMessage') ? I18n.t('topic.invite_private.error') : I18n.t('topic.invite_reply.error'); return this.get('isMessage') ? I18n.t('topic.invite_private.error') : I18n.t('topic.invite_reply.error');
}.property('isMessage'), }.property('isMessage'),
placeholderKey: function() { @computed('model.details.can_invite_via_email')
return (Discourse.SiteSettings.enable_sso || !Discourse.SiteSettings.enable_local_logins) ? placeholderKey(can_invite_via_email) {
'topic.invite_reply.username_placeholder' : return (can_invite_via_email) ?
'topic.invite_private.email_or_username_placeholder'; 'topic.invite_private.email_or_username_placeholder' :
}.property(), 'topic.invite_reply.username_placeholder';
},
customMessagePlaceholder: function() { customMessagePlaceholder: function() {
return I18n.t('invite.custom_message_placeholder'); return I18n.t('invite.custom_message_placeholder');

View File

@ -59,7 +59,7 @@ class Invite < ActiveRecord::Base
if topic.private_message? if topic.private_message?
topic.grant_permission_to_user(user.email) topic.grant_permission_to_user(user.email)
elsif topic.category && topic.category.groups.any? elsif topic.category && topic.category.groups.any?
if Guardian.new(invited_by).can_invite_to?(topic) && !SiteSetting.enable_sso if Guardian.new(invited_by).can_invite_via_email?(topic)
(topic.category.groups - user.groups).each do |group| (topic.category.groups - user.groups).each do |group|
group.add(user) group.add(user)
GroupActionLogger.new(Discourse.system_user, group).log_add_user_to_group(user) GroupActionLogger.new(Discourse.system_user, group).log_add_user_to_group(user)

View File

@ -744,7 +744,7 @@ SQL
end end
end end
if username_or_email =~ /^.+@.+$/ && !SiteSetting.enable_sso && SiteSetting.enable_local_logins if username_or_email =~ /^.+@.+$/ && Guardian.new(invited_by).can_invite_via_email?(self)
# rate limit topic invite # rate limit topic invite
RateLimiter.new(invited_by, "topic-invitations-per-day", SiteSetting.max_topic_invitations_per_day, 1.day.to_i).performed! RateLimiter.new(invited_by, "topic-invitations-per-day", SiteSetting.max_topic_invitations_per_day, 1.day.to_i).performed!

View File

@ -115,6 +115,7 @@ class TopicViewSerializer < ApplicationSerializer
result[:can_recover] = true if scope.can_recover_topic?(object.topic) result[:can_recover] = true if scope.can_recover_topic?(object.topic)
result[:can_remove_allowed_users] = true if scope.can_remove_allowed_users?(object.topic) result[:can_remove_allowed_users] = true if scope.can_remove_allowed_users?(object.topic)
result[:can_invite_to] = true if scope.can_invite_to?(object.topic) result[:can_invite_to] = true if scope.can_invite_to?(object.topic)
result[:can_invite_via_email] = true if scope.can_invite_via_email?(object.topic)
result[:can_create_post] = true if scope.can_create?(Post, object.topic) result[:can_create_post] = true if scope.can_create?(Post, object.topic)
result[:can_reply_as_new_topic] = true if scope.can_reply_as_new_topic?(object.topic) result[:can_reply_as_new_topic] = true if scope.can_reply_as_new_topic?(object.topic)
result[:can_flag_topic] = actions_summary.any? { |a| a[:can_act] } result[:can_flag_topic] = actions_summary.any? { |a| a[:can_act] }

View File

@ -229,7 +229,6 @@ class Guardian
def can_invite_to?(object, group_ids=nil) def can_invite_to?(object, group_ids=nil)
return false if ! authenticated? return false if ! authenticated?
return false unless (!SiteSetting.must_approve_users? || is_staff?)
return true if is_admin? return true if is_admin?
return false if (SiteSetting.max_invites_per_day.to_i == 0 && !is_staff?) return false if (SiteSetting.max_invites_per_day.to_i == 0 && !is_staff?)
return false if ! can_see?(object) return false if ! can_see?(object)
@ -244,6 +243,11 @@ class Guardian
user.has_trust_level?(TrustLevel[2]) user.has_trust_level?(TrustLevel[2])
end end
def can_invite_via_email?(object)
return false unless can_invite_to?(object)
!SiteSetting.enable_sso && SiteSetting.enable_local_logins && (!SiteSetting.must_approve_users? || is_staff?)
end
def can_bulk_invite_to_forum?(user) def can_bulk_invite_to_forum?(user)
user.admin? user.admin?
end end

View File

@ -341,16 +341,6 @@ describe Guardian do
expect(Guardian.new(moderator).can_invite_to?(topic)).to be_truthy expect(Guardian.new(moderator).can_invite_to?(topic)).to be_truthy
end end
it 'returns true when the site requires approving users and is mod' do
SiteSetting.expects(:must_approve_users?).returns(true)
expect(Guardian.new(moderator).can_invite_to?(topic)).to be_truthy
end
it 'returns false when the site requires approving users and is regular' do
SiteSetting.expects(:must_approve_users?).returns(true)
expect(Guardian.new(coding_horror).can_invite_to?(topic)).to be_falsey
end
it 'returns false for normal user on private topic' do it 'returns false for normal user on private topic' do
expect(Guardian.new(user).can_invite_to?(private_topic)).to be_falsey expect(Guardian.new(user).can_invite_to?(private_topic)).to be_falsey
end end
@ -364,6 +354,38 @@ describe Guardian do
end end
end end
describe 'can_invite_via_email?' do
it 'returns true for all (tl2 and above) users when sso is disabled, local logins are enabled, user approval is not required' do
expect(Guardian.new(trust_level_2).can_invite_via_email?(topic)).to be_truthy
expect(Guardian.new(moderator).can_invite_via_email?(topic)).to be_truthy
expect(Guardian.new(admin).can_invite_via_email?(topic)).to be_truthy
end
it 'returns false for all users when sso is enabled' do
SiteSetting.enable_sso = true
expect(Guardian.new(trust_level_2).can_invite_via_email?(topic)).to be_falsey
expect(Guardian.new(moderator).can_invite_via_email?(topic)).to be_falsey
expect(Guardian.new(admin).can_invite_via_email?(topic)).to be_falsey
end
it 'returns false for all users when local logins are disabled' do
SiteSetting.enable_local_logins = false
expect(Guardian.new(trust_level_2).can_invite_via_email?(topic)).to be_falsey
expect(Guardian.new(moderator).can_invite_via_email?(topic)).to be_falsey
expect(Guardian.new(admin).can_invite_via_email?(topic)).to be_falsey
end
it 'returns correct valuse when user approval is required' do
SiteSetting.must_approve_users = true
expect(Guardian.new(trust_level_2).can_invite_via_email?(topic)).to be_falsey
expect(Guardian.new(moderator).can_invite_via_email?(topic)).to be_truthy
expect(Guardian.new(admin).can_invite_via_email?(topic)).to be_truthy
end
end
describe 'can_see?' do describe 'can_see?' do
it 'returns false with a nil object' do it 'returns false with a nil object' do

View File

@ -73,7 +73,8 @@ describe InviteMailer do
context "invite to topic" do context "invite to topic" do
let(:topic) { Fabricate(:topic, excerpt: "Topic invite support is now available in Discourse!") } let(:trust_level_2) { build(:user, trust_level: 2) }
let(:topic) { Fabricate(:topic, excerpt: "Topic invite support is now available in Discourse!", user: trust_level_2) }
let(:invite) { topic.invite(topic.user, 'name@example.com') } let(:invite) { topic.invite(topic.user, 'name@example.com') }
context "default invite message" do context "default invite message" do

View File

@ -330,25 +330,24 @@ describe Invite do
end end
context 'invited to topics' do context 'invited to topics' do
let!(:topic) { Fabricate(:private_message_topic) } let(:tl2_user) { Fabricate(:user, trust_level: 2) }
let!(:topic) { Fabricate(:private_message_topic, user: tl2_user) }
let!(:invite) { let!(:invite) {
topic.invite(topic.user, 'jake@adventuretime.ooo') topic.invite(topic.user, 'jake@adventuretime.ooo')
} }
context 'redeem topic invite' do context 'redeem topic invite' do
it 'adds the user to the topic_users' do it 'adds the user to the topic_users' do
user = invite.redeem user = invite.redeem
topic.reload topic.reload
expect(topic.allowed_users.include?(user)).to eq(true) expect(topic.allowed_users.include?(user)).to eq(true)
expect(Guardian.new(user).can_see?(topic)).to eq(true) expect(Guardian.new(user).can_see?(topic)).to eq(true)
end end
end end
context 'invited by another user to the same topic' do context 'invited by another user to the same topic' do
let(:coding_horror) { User.find_by(username: "CodingHorror") } let(:another_tl2_user) { Fabricate(:user, trust_level: 2) }
let!(:another_invite) { topic.invite(coding_horror, 'jake@adventuretime.ooo') } let!(:another_invite) { topic.invite(another_tl2_user, 'jake@adventuretime.ooo') }
let!(:user) { invite.redeem } let!(:user) { invite.redeem }
it 'adds the user to the topic_users' do it 'adds the user to the topic_users' do
@ -358,25 +357,14 @@ describe Invite do
end end
context 'invited by another user to a different topic' do context 'invited by another user to a different topic' do
let!(:another_invite) { another_topic.invite(coding_horror, 'jake@adventuretime.ooo') }
let!(:user) { invite.redeem } let!(:user) { invite.redeem }
let(:another_tl2_user) { Fabricate(:user, trust_level: 2) }
let(:coding_horror) { User.find_by(username: "CodingHorror") } let(:another_topic) { Fabricate(:topic, user: another_tl2_user) }
let(:another_topic) { Fabricate(:topic, category_id: nil, archetype: "private_message", user: coding_horror) }
it 'adds the user to the topic_users of the first topic' do it 'adds the user to the topic_users of the first topic' do
expect(another_topic.invite(another_tl2_user, user.username)).to be_truthy # invited via username
expect(topic.allowed_users.include?(user)).to eq(true) expect(topic.allowed_users.include?(user)).to eq(true)
expect(another_topic.allowed_users.include?(user)).to eq(true) expect(another_topic.allowed_users.include?(user)).to eq(true)
duplicate_invite = Invite.find_by(id: another_invite.id)
expect(duplicate_invite).to be_nil
end
context 'if they redeem the other invite afterwards' do
it 'wont redeem a duplicate invite' do
expect(another_invite.redeem).to be_blank
end
end end
end end
end end

View File

@ -502,28 +502,33 @@ describe Topic do
end end
it "rate limits topic invitations" do context 'rate limits' do
SiteSetting.stubs(:max_topic_invitations_per_day).returns(2)
RateLimiter.stubs(:disabled?).returns(false)
RateLimiter.clear_all!
start = Time.now.tomorrow.beginning_of_day it "rate limits topic invitations" do
freeze_time(start) SiteSetting.stubs(:max_topic_invitations_per_day).returns(2)
RateLimiter.stubs(:disabled?).returns(false)
RateLimiter.clear_all!
user = Fabricate(:user) start = Time.now.tomorrow.beginning_of_day
topic = Fabricate(:topic) freeze_time(start)
freeze_time(start + 10.minutes) user = Fabricate(:user)
topic.invite(topic.user, user.username) trust_level_2 = Fabricate(:user, trust_level: 2)
topic = Fabricate(:topic, user: trust_level_2)
freeze_time(start + 20.minutes) freeze_time(start + 10.minutes)
topic.invite(topic.user, "walter@white.com") topic.invite(topic.user, user.username)
freeze_time(start + 30.minutes) freeze_time(start + 20.minutes)
topic.invite(topic.user, "walter@white.com")
freeze_time(start + 30.minutes)
expect {
topic.invite(topic.user, "user@example.com")
}.to raise_error(RateLimiter::LimitExceeded)
end
expect {
topic.invite(topic.user, "user@example.com")
}.to raise_error(RateLimiter::LimitExceeded)
end end
context 'bumping topics' do context 'bumping topics' do