feat(login by email option): added ability to log in with user primary email. Fixes: RCCE-63

This commit is contained in:
Serhii Ilin 2024-03-14 11:18:07 +02:00
parent 5f36eb1b22
commit d8281bb5c0
7 changed files with 28 additions and 14 deletions

View file

@ -80,6 +80,18 @@ class TestLoginController(object):
assert username == 'test_regular'
response.mustcontain('logout')
def test_login_with_primary_email(self):
user_email = 'test_regular@mail.com'
response = self.app.post(route_path('login'),
{'username': user_email,
'password': 'test12'}, status=302)
response = response.follow()
session = response.get_session_from_response()
user = session['rhodecode_user']
assert user['username'] == user_email.split('@')[0]
assert user['is_authenticated']
response.mustcontain('logout')
def test_login_regular_forbidden_when_super_admin_restriction(self):
from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
with fixture.auth_restriction(self.app._pyramid_registry,

View file

@ -54,8 +54,8 @@ CaptchaData = collections.namedtuple(
'CaptchaData', 'active, private_key, public_key')
def store_user_in_session(session, username, remember=False):
user = User.get_by_username(username, case_insensitive=True)
def store_user_in_session(session, user_identifier, remember=False):
user = User.get_by_username_or_primary_email(user_identifier)
auth_user = AuthUser(user.user_id)
auth_user.set_authenticated()
cs = auth_user.get_cookie_store()
@ -74,7 +74,7 @@ def store_user_in_session(session, username, remember=False):
safe_cs = cs.copy()
safe_cs['password'] = '****'
log.info('user %s is now authenticated and stored in '
'session, session attrs %s', username, safe_cs)
'session, session attrs %s', user_identifier, safe_cs)
# dumps session attrs back to cookie
session._update_cookie_out()
@ -181,7 +181,7 @@ class LoginView(BaseAppView):
# form checks for username/password, now we're authenticated
headers = store_user_in_session(
self.session,
username=form_result['username'],
user_identifier=form_result['username'],
remember=form_result['remember'])
log.debug('Redirecting to "%s" after login.', c.came_from)

View file

@ -389,11 +389,7 @@ class RhodeCodeAuthPluginBase(object):
log.debug(
'Trying to fetch user `%s` from RhodeCode database', username)
if username:
user = User.get_by_username(username)
if not user:
log.debug('User not found, fallback to fetch user in '
'case insensitive mode')
user = User.get_by_username(username, case_insensitive=True)
user = User.get_by_username_or_primary_email(username)
else:
log.debug('provided username:`%s` is empty skipping...', username)
if not user:

View file

@ -169,7 +169,7 @@ class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode_anon", "username": userobj.username})
return user_attrs
elif userobj.username == username and password_match:
elif (userobj.username == username or userobj.email == username) and password_match:
log.info('user `%s` authenticated correctly', userobj.username,
extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode", "username": userobj.username})
return user_attrs

View file

@ -35,7 +35,7 @@ import collections
from sqlalchemy import (
or_, and_, not_, func, cast, TypeDecorator, event, select,
true, false, null,
true, false, null, union_all,
Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
Text, Float, PickleType, BigInteger)
@ -953,6 +953,12 @@ class User(Base, BaseModel):
return cls.execute(q).scalar_one_or_none()
@classmethod
def get_by_username_or_primary_email(cls, user_identifier):
qs = union_all(cls.select().where(func.lower(cls.username) == func.lower(user_identifier)),
cls.select().where(func.lower(cls.email) == func.lower(user_identifier)))
return cls.execute(cls.select(User).from_statement(qs)).scalar_one_or_none()
@classmethod
def get_by_auth_token(cls, auth_token, cache=False):

View file

@ -432,7 +432,7 @@ def ValidAuth(localizer):
if not authenticate(username, password, '', HTTP_TYPE,
skip_missing=True):
user = User.get_by_username(username)
user = User.get_by_username_or_primary_email(username)
if user and not user.active:
log.warning('user %s is disabled', username)
msg = M(self, 'disabled_account', state)

View file

@ -35,12 +35,12 @@
<%block name="above_login_button" />
<!-- login -->
<div class="sign-in-title">
<h1>${_('Sign In using username/password')}</h1>
<h1>${_('Sign In using credentials')}</h1>
</div>
<div class="inner form">
${h.form(request.route_path('login', _query={'came_from': c.came_from}), needs_csrf_token=False)}
<label for="username">${_('Username')}:</label>
<label for="username">${_('Username or email address')}:</label>
${h.text('username', class_='focus', value=defaults.get('username'))}
%if 'username' in errors:
<span class="error-message">${errors.get('username')}</span>