Skip to content
Snippets Groups Projects
Commit 94069d38 authored by Jarka Košanová's avatar Jarka Košanová :palm_tree:
Browse files

Support password reset from any verified email

- any email a user enters in forgotten password form
is used if it is verified
- Devise overriden coden in RecoverableByAnyEmail concern

Changelog: added
parent 3750e507
No related branches found
No related tags found
1 merge request!119231Support password reset from any verified email
# frozen_string_literal: true
# Concern that overrides the Devise methods
# to send reset password instructions to any verified user email
module RecoverableByAnyEmail
extend ActiveSupport::Concern
class_methods do
def send_reset_password_instructions(attributes = {})
return super unless Feature.enabled?(:password_reset_any_verified_email)
email = attributes.delete(:email)
super unless email
recoverable = by_email_with_errors(email)
recoverable.send_reset_password_instructions(to: email) if recoverable&.persisted?
recoverable
end
private
def by_email_with_errors(email)
record = find_by_any_email(email, confirmed: true) || new
record.errors.add(:email, :invalid) unless record.persisted?
record
end
end
def send_reset_password_instructions(opts = {})
return super() unless Feature.enabled?(:password_reset_any_verified_email)
token = set_reset_password_token
send_reset_password_instructions_notification(token, opts)
token
end
private
def send_reset_password_instructions_notification(token, opts = {})
return super(token) unless Feature.enabled?(:password_reset_any_verified_email)
send_devise_notification(:reset_password_instructions, token, opts)
end
end
......@@ -91,6 +91,7 @@ class User < ApplicationRecord
# Must be included after `devise`
include EncryptedUserPassword
include RecoverableByAnyEmail
include AdminChangedPasswordNotifier
......
---
name: password_reset_any_verified_email
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119231
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/410038
milestone: '16.0'
type: development
group: group::authentication and authorization
default_enabled: false
......@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'User password', feature_category: :system_access do
include EmailHelpers
describe 'send password reset' do
context 'when recaptcha is enabled' do
before do
......@@ -26,5 +28,57 @@
expect(page).not_to have_css('.g-recaptcha')
end
end
context 'when user has multiple emails' do
let_it_be(:user) { create(:user, email: 'primary@example.com') }
let_it_be(:verified_email) { create(:email, :confirmed, user: user, email: 'second@example.com') }
let_it_be(:unverified_email) { create(:email, user: user, email: 'unverified@example.com') }
let(:ff_enabled) { true }
before do
stub_feature_flags(password_reset_any_verified_email: ff_enabled)
perform_enqueued_jobs do
visit new_user_password_path
fill_in 'user_email', with: email
click_button 'Reset password'
end
end
context 'when user enters the primary email' do
let(:email) { user.email }
it 'send the email to the correct email address' do
expect(ActionMailer::Base.deliveries.first.to).to include(email)
end
end
context 'when user enters a secondary verified email' do
let(:email) { verified_email.email }
context 'when password_reset_any_verified_email FF is enabled' do
it 'send the email to the correct email address' do
expect(ActionMailer::Base.deliveries.first.to).to include(email)
end
end
context 'when password_reset_any_verified_email FF is not enabled' do
let(:ff_enabled) { false }
it 'does not send an email' do
expect(ActionMailer::Base.deliveries.count).to eq(0)
end
end
end
context 'when user enters an unverified email' do
let(:email) { unverified_email.email }
it 'does not send an email' do
expect(ActionMailer::Base.deliveries.count).to eq(0)
end
end
end
end
end
......@@ -102,9 +102,12 @@
end
describe '#reset_password_instructions' do
subject { described_class.reset_password_instructions(user, 'faketoken') }
let_it_be(:user) { create(:user) }
let(:params) { {} }
subject do
described_class.reset_password_instructions(user, 'faketoken', params)
end
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
......@@ -135,6 +138,15 @@
it 'has the mailgun suppression bypass header' do
is_expected.to have_header 'X-Mailgun-Suppressions-Bypass', 'true'
end
context 'with email in params' do
let(:email) { 'example@example.com' }
let(:params) { { to: email } }
it 'is sent to the specified email' do
is_expected.to deliver_to email
end
end
end
describe '#email_changed' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe RecoverableByAnyEmail, feature_category: :system_access do
describe '.send_reset_password_instructions' do
let_it_be(:user) { create(:user, email: 'test@example.com') }
let_it_be(:verified_email) { create(:email, :confirmed, user: user) }
let_it_be(:unverified_email) { create(:email, user: user) }
let(:ff_enabled) { true }
before do
stub_feature_flags(password_reset_any_verified_email: ff_enabled)
end
subject(:send_reset_password_instructions) do
User.send_reset_password_instructions(email: email)
end
shared_examples 'sends the password reset email' do
it 'finds the user' do
expect(send_reset_password_instructions).to eq(user)
end
it 'sends the email' do
expect { send_reset_password_instructions }.to have_enqueued_mail(DeviseMailer, :reset_password_instructions)
end
end
shared_examples 'does not send the password reset email' do
it 'does not find the user' do
expect(subject.id).to be_nil
expect(subject.errors).not_to be_empty
end
it 'does not send any email' do
subject
expect { subject }.not_to have_enqueued_mail(DeviseMailer, :reset_password_instructions)
end
end
context 'with user primary email' do
let(:email) { user.email }
it_behaves_like 'sends the password reset email'
end
context 'with user verified email' do
let(:email) { verified_email.email }
context 'when password_reset_any_verified_email FF is enabled' do
it_behaves_like 'sends the password reset email'
end
context 'when password_reset_any_verified_email FF is not enabled' do
let(:ff_enabled) { false }
it_behaves_like 'does not send the password reset email'
end
end
context 'with user unverified email' do
let(:email) { unverified_email.email }
it_behaves_like 'does not send the password reset email'
end
end
describe '#send_reset_password_instructions' do
let_it_be(:user) { create(:user) }
let_it_be(:opts) { { email: 'random@email.com' } }
let_it_be(:token) { 'passwordresettoken' }
before do
stub_feature_flags(password_reset_any_verified_email: ff_enabled)
allow(user).to receive(:set_reset_password_token).and_return(token)
end
subject { user.send_reset_password_instructions(opts) }
context 'when password_reset_any_verified_email FF is not enabled' do
let(:ff_enabled) { false }
# original Devise behavior
it 'calls send_reset_password_instructions_notification just with token' do
expect(user).to receive(:send_reset_password_instructions_notification).with(token)
subject
end
end
context 'when password_reset_any_verified_email FF is enabled' do
let(:ff_enabled) { true }
it 'sends the email' do
expect { subject }.to have_enqueued_mail(DeviseMailer, :reset_password_instructions)
end
it 'calls send_reset_password_instructions_notification with correct arguments' do
expect(user).to receive(:send_reset_password_instructions_notification).with(token, opts)
subject
end
it 'returns the generated token' do
expect(subject).to eq(token)
end
end
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment