mirror of
https://github.com/discourse/discourse.git
synced 2024-11-30 19:15:32 +08:00
9174716737
This method is a huge footgun in production, since it calls
the Redis KEYS command. From the Redis documentation at
https://redis.io/commands/keys/:
> Warning: consider KEYS as a command that should only be used in
production environments with extreme care. It may ruin performance when
it is executed against large databases. This command is intended for
debugging and special operations, such as changing your keyspace layout.
Don't use KEYS in your regular application code.
Since we were only using `delete_prefixed` in specs (now that we
removed the usage in production in 24ec06ff85
)
we can remove this and instead rely on `use_redis_snapshotting` on the
particular tests that need this kind of clearing functionality.
457 lines
16 KiB
Ruby
457 lines
16 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "rotp"
|
|
|
|
RSpec.describe UsersEmailController do
|
|
fab!(:user) { Fabricate(:user) }
|
|
let!(:email_token) { Fabricate(:email_token, user: user) }
|
|
fab!(:moderator) { Fabricate(:moderator) }
|
|
|
|
describe "#confirm-new-email" do
|
|
it "does not redirect to login for signed out accounts, this route works fine as anon user" do
|
|
get "/u/confirm-new-email/invalidtoken"
|
|
|
|
expect(response.status).to eq(200)
|
|
end
|
|
|
|
it "does not redirect to login for signed out accounts on login_required sites, this route works fine as anon user" do
|
|
SiteSetting.login_required = true
|
|
get "/u/confirm-new-email/invalidtoken"
|
|
|
|
expect(response.status).to eq(200)
|
|
end
|
|
|
|
it "errors out for invalid tokens" do
|
|
sign_in(user)
|
|
|
|
get "/u/confirm-new-email/invalidtoken"
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.body).to include(I18n.t("change_email.already_done"))
|
|
end
|
|
|
|
it "does not change email if accounts mismatch for a signed in user" do
|
|
updater = EmailUpdater.new(guardian: user.guardian, user: user)
|
|
updater.change_to("bubblegum@adventuretime.ooo")
|
|
|
|
old_email = user.email
|
|
|
|
sign_in(moderator)
|
|
|
|
put "/u/confirm-new-email", params: { token: "#{email_token.token}" }
|
|
expect(user.reload.email).to eq(old_email)
|
|
end
|
|
|
|
context "with a valid user" do
|
|
let(:updater) { EmailUpdater.new(guardian: user.guardian, user: user) }
|
|
|
|
before do
|
|
sign_in(user)
|
|
updater.change_to("bubblegum@adventuretime.ooo")
|
|
end
|
|
|
|
it "includes security_key_allowed_credential_ids in a hidden field" do
|
|
key1 = Fabricate(:user_security_key_with_random_credential, user: user)
|
|
key2 = Fabricate(:user_security_key_with_random_credential, user: user)
|
|
|
|
get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
|
|
|
|
doc = Nokogiri.HTML5(response.body)
|
|
credential_ids = doc.css("#security-key-allowed-credential-ids").first["value"].split(",")
|
|
expect(credential_ids).to contain_exactly(key1.credential_id, key2.credential_id)
|
|
end
|
|
|
|
it "confirms with a correct token" do
|
|
user.user_stat.update_columns(bounce_score: 42, reset_bounce_score_after: 1.week.from_now)
|
|
|
|
put "/u/confirm-new-email", params: { token: "#{updater.change_req.new_email_token.token}" }
|
|
|
|
expect(response.status).to eq(302)
|
|
expect(response.redirect_url).to include("done")
|
|
user.reload
|
|
expect(user.user_stat.bounce_score).to eq(0)
|
|
expect(user.user_stat.reset_bounce_score_after).to eq(nil)
|
|
expect(user.email).to eq("bubblegum@adventuretime.ooo")
|
|
end
|
|
|
|
context "when second factor is required" do
|
|
fab!(:second_factor) { Fabricate(:user_second_factor_totp, user: user) }
|
|
fab!(:backup_code) { Fabricate(:user_second_factor_backup, user: user) }
|
|
|
|
it "requires a second factor token" do
|
|
get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.body).to include(I18n.t("login.second_factor_title"))
|
|
expect(response.body).not_to include(I18n.t("login.invalid_second_factor_code"))
|
|
end
|
|
|
|
it "requires a backup token" do
|
|
get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}?show_backup=true"
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.body).to include(I18n.t("login.second_factor_backup_title"))
|
|
end
|
|
|
|
it "adds an error on a second factor attempt" do
|
|
put "/u/confirm-new-email",
|
|
params: {
|
|
token: updater.change_req.new_email_token.token,
|
|
second_factor_token: "000000",
|
|
second_factor_method: UserSecondFactor.methods[:totp],
|
|
}
|
|
|
|
expect(response.status).to eq(302)
|
|
expect(flash[:invalid_second_factor]).to eq(true)
|
|
end
|
|
|
|
it "confirms with a correct second token" do
|
|
put "/u/confirm-new-email",
|
|
params: {
|
|
second_factor_token: ROTP::TOTP.new(second_factor.data).now,
|
|
second_factor_method: UserSecondFactor.methods[:totp],
|
|
token: updater.change_req.new_email_token.token,
|
|
}
|
|
|
|
expect(response.status).to eq(302)
|
|
expect(user.reload.email).to eq("bubblegum@adventuretime.ooo")
|
|
end
|
|
|
|
context "with rate limiting" do
|
|
before { RateLimiter.enable }
|
|
|
|
use_redis_snapshotting
|
|
|
|
it "rate limits by IP" do
|
|
freeze_time
|
|
|
|
6.times do
|
|
put "/u/confirm-new-email",
|
|
params: {
|
|
token: "blah",
|
|
second_factor_token: "000000",
|
|
second_factor_method: UserSecondFactor.methods[:totp],
|
|
}
|
|
|
|
expect(response.status).to eq(302)
|
|
end
|
|
|
|
put "/u/confirm-new-email",
|
|
params: {
|
|
token: "blah",
|
|
second_factor_token: "000000",
|
|
second_factor_method: UserSecondFactor.methods[:totp],
|
|
}
|
|
|
|
expect(response.status).to eq(429)
|
|
end
|
|
|
|
it "rate limits by username" do
|
|
freeze_time
|
|
|
|
6.times do |x|
|
|
user.email_change_requests.last.update(
|
|
change_state: EmailChangeRequest.states[:complete],
|
|
)
|
|
put "/u/confirm-new-email",
|
|
params: {
|
|
token: updater.change_req.new_email_token.token,
|
|
second_factor_token: "000000",
|
|
second_factor_method: UserSecondFactor.methods[:totp],
|
|
},
|
|
env: {
|
|
REMOTE_ADDR: "1.2.3.#{x}",
|
|
}
|
|
|
|
expect(response.status).to eq(302)
|
|
end
|
|
|
|
user.email_change_requests.last.update(
|
|
change_state: EmailChangeRequest.states[:authorizing_new],
|
|
)
|
|
put "/u/confirm-new-email",
|
|
params: {
|
|
token: updater.change_req.new_email_token.token,
|
|
second_factor_token: "000000",
|
|
second_factor_method: UserSecondFactor.methods[:totp],
|
|
},
|
|
env: {
|
|
REMOTE_ADDR: "1.2.3.4",
|
|
}
|
|
|
|
expect(response.status).to eq(429)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when security key is required" do
|
|
fab!(:user_security_key) do
|
|
Fabricate(
|
|
:user_security_key,
|
|
user: user,
|
|
credential_id: valid_security_key_data[:credential_id],
|
|
public_key: valid_security_key_data[:public_key],
|
|
)
|
|
end
|
|
|
|
before { simulate_localhost_webauthn_challenge }
|
|
|
|
it "requires a security key" do
|
|
get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.body).to include(I18n.t("login.security_key_authenticate"))
|
|
expect(response.body).to include(I18n.t("login.security_key_description"))
|
|
end
|
|
|
|
context "if the user has a TOTP enabled and wants to use that instead" do
|
|
before { Fabricate(:user_second_factor_totp, user: user) }
|
|
|
|
it "allows entering the totp code instead" do
|
|
get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}?show_totp=true"
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.body).to include(I18n.t("login.second_factor_title"))
|
|
expect(response.body).not_to include(I18n.t("login.security_key_authenticate"))
|
|
end
|
|
end
|
|
|
|
it "adds an error on a security key attempt" do
|
|
get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
|
|
put "/u/confirm-new-email",
|
|
params: {
|
|
token: updater.change_req.new_email_token.token,
|
|
second_factor_token: "{}",
|
|
second_factor_method: UserSecondFactor.methods[:security_key],
|
|
}
|
|
|
|
expect(response.status).to eq(302)
|
|
expect(flash[:invalid_second_factor]).to eq(true)
|
|
end
|
|
|
|
it "confirms with a correct security key token" do
|
|
get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
|
|
put "/u/confirm-new-email",
|
|
params: {
|
|
token: updater.change_req.new_email_token.token,
|
|
second_factor_token: valid_security_key_auth_post_data.to_json,
|
|
second_factor_method: UserSecondFactor.methods[:security_key],
|
|
}
|
|
|
|
expect(response.status).to eq(302)
|
|
expect(user.reload.email).to eq("bubblegum@adventuretime.ooo")
|
|
end
|
|
|
|
context "if the security key data JSON is garbled" do
|
|
it "raises an invalid parameters error" do
|
|
get "/u/confirm-new-email/#{updater.change_req.new_email_token.token}"
|
|
put "/u/confirm-new-email",
|
|
params: {
|
|
token: updater.change_req.new_email_token.token,
|
|
second_factor_token: "{someweird: 8notjson}",
|
|
second_factor_method: UserSecondFactor.methods[:security_key],
|
|
}
|
|
|
|
expect(response.status).to eq(400)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
it "destroys email tokens associated with the old email after the new email is confirmed" do
|
|
SiteSetting.enable_secondary_emails = true
|
|
|
|
email_token =
|
|
user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:password_reset])
|
|
|
|
updater = EmailUpdater.new(guardian: user.guardian, user: user)
|
|
updater.change_to("bubblegum@adventuretime.ooo")
|
|
|
|
sign_in(user)
|
|
put "/u/confirm-new-email", params: { token: "#{updater.change_req.new_email_token.token}" }
|
|
|
|
new_password = SecureRandom.hex
|
|
put "/u/password-reset/#{email_token.token}.json", params: { password: new_password }
|
|
expect(response.parsed_body["success"]).to eq(false)
|
|
expect(response.parsed_body["message"]).to eq(
|
|
I18n.t("password_reset.no_token", base_url: Discourse.base_url),
|
|
)
|
|
expect(user.reload.confirm_password?(new_password)).to eq(false)
|
|
end
|
|
end
|
|
|
|
describe "#confirm-old-email" do
|
|
it "redirects to login for signed out accounts" do
|
|
get "/u/confirm-old-email/invalidtoken"
|
|
|
|
expect(response.status).to eq(302)
|
|
expect(response.redirect_url).to eq("http://test.localhost/login")
|
|
end
|
|
|
|
it "errors out for invalid tokens" do
|
|
sign_in(user)
|
|
|
|
get "/u/confirm-old-email/invalidtoken"
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.body).to include(I18n.t("change_email.already_done"))
|
|
end
|
|
|
|
it "bans change when accounts do not match" do
|
|
sign_in(user)
|
|
updater = EmailUpdater.new(guardian: moderator.guardian, user: moderator)
|
|
email_change_request = updater.change_to("bubblegum@adventuretime.ooo")
|
|
|
|
get "/u/confirm-old-email/#{email_change_request.old_email_token.token}"
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(body).to include("alert-error")
|
|
end
|
|
|
|
context "with valid old token" do
|
|
it "confirms with a correct token" do
|
|
sign_in(moderator)
|
|
updater = EmailUpdater.new(guardian: moderator.guardian, user: moderator)
|
|
email_change_request = updater.change_to("bubblegum@adventuretime.ooo")
|
|
|
|
get "/u/confirm-old-email/#{email_change_request.old_email_token.token}"
|
|
|
|
expect(response.status).to eq(200)
|
|
body = CGI.unescapeHTML(response.body)
|
|
expect(body).to include(I18n.t("change_email.authorizing_old.title"))
|
|
expect(body).to include(I18n.t("change_email.authorizing_old.description"))
|
|
|
|
put "/u/confirm-old-email", params: { token: email_change_request.old_email_token.token }
|
|
|
|
expect(response.status).to eq(302)
|
|
expect(response.redirect_url).to include("done=true")
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#create" do
|
|
it "has an email token" do
|
|
sign_in(user)
|
|
|
|
expect {
|
|
post "/u/#{user.username}/preferences/email.json",
|
|
params: {
|
|
email: "bubblegum@adventuretime.ooo",
|
|
}
|
|
}.to change(EmailChangeRequest, :count)
|
|
|
|
emailChangeRequest = EmailChangeRequest.last
|
|
expect(emailChangeRequest.old_email).to eq(nil)
|
|
expect(emailChangeRequest.new_email).to eq("bubblegum@adventuretime.ooo")
|
|
end
|
|
end
|
|
|
|
describe "#update" do
|
|
it "requires you to be logged in" do
|
|
put "/u/#{user.username}/preferences/email.json",
|
|
params: {
|
|
email: "bubblegum@adventuretime.ooo",
|
|
}
|
|
expect(response.status).to eq(403)
|
|
end
|
|
|
|
context "when logged in" do
|
|
before { sign_in(user) }
|
|
|
|
it "raises an error without an email parameter" do
|
|
put "/u/#{user.username}/preferences/email.json"
|
|
expect(response.status).to eq(400)
|
|
end
|
|
|
|
it "raises an error without an invalid email" do
|
|
put "/u/#{user.username}/preferences/email.json", params: { email: "sam@not-email.com'" }
|
|
expect(response.status).to eq(422)
|
|
expect(response.body).to include("Email is invalid")
|
|
end
|
|
|
|
it "raises an error if you can't edit the user's email" do
|
|
SiteSetting.email_editable = false
|
|
|
|
put "/u/#{user.username}/preferences/email.json",
|
|
params: {
|
|
email: "bubblegum@adventuretime.ooo",
|
|
}
|
|
expect(response).to be_forbidden
|
|
end
|
|
|
|
context "when the new email address is taken" do
|
|
fab!(:other_user) { Fabricate(:coding_horror) }
|
|
|
|
context "when hide_email_address_taken is disabled" do
|
|
before { SiteSetting.hide_email_address_taken = false }
|
|
|
|
it "raises an error" do
|
|
put "/u/#{user.username}/preferences/email.json", params: { email: other_user.email }
|
|
expect(response).to_not be_successful
|
|
end
|
|
|
|
it "raises an error if there is whitespace too" do
|
|
put "/u/#{user.username}/preferences/email.json",
|
|
params: {
|
|
email: "#{other_user.email} ",
|
|
}
|
|
expect(response).to_not be_successful
|
|
end
|
|
end
|
|
|
|
context "when hide_email_address_taken is enabled" do
|
|
before { SiteSetting.hide_email_address_taken = true }
|
|
|
|
it "responds with success" do
|
|
put "/u/#{user.username}/preferences/email.json", params: { email: other_user.email }
|
|
expect(response.status).to eq(200)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when new email is different case of existing email" do
|
|
fab!(:other_user) { Fabricate(:user, email: "case.insensitive@gmail.com") }
|
|
|
|
it "raises an error" do
|
|
put "/u/#{user.username}/preferences/email.json",
|
|
params: {
|
|
email: other_user.email.upcase,
|
|
}
|
|
expect(response).to_not be_successful
|
|
end
|
|
end
|
|
|
|
it "raises an error when new email domain is present in blocked_email_domains site setting" do
|
|
SiteSetting.blocked_email_domains = "mailinator.com"
|
|
|
|
put "/u/#{user.username}/preferences/email.json",
|
|
params: {
|
|
email: "not_good@mailinator.com",
|
|
}
|
|
expect(response).to_not be_successful
|
|
end
|
|
|
|
it "raises an error when new email domain is not present in allowed_email_domains site setting" do
|
|
SiteSetting.allowed_email_domains = "discourse.org"
|
|
|
|
put "/u/#{user.username}/preferences/email.json",
|
|
params: {
|
|
email: "bubblegum@adventuretime.ooo",
|
|
}
|
|
expect(response).to_not be_successful
|
|
end
|
|
|
|
context "with success" do
|
|
it "has an email token" do
|
|
expect do
|
|
put "/u/#{user.username}/preferences/email.json",
|
|
params: {
|
|
email: "bubblegum@adventuretime.ooo",
|
|
}
|
|
end.to change(EmailChangeRequest, :count)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|