Skip to content

Commit 5ec9f21

Browse files
committed
Add a unit test triggering a bug in IP autologin, and fix the bug
Contains_eager must be used when an explicit join has already been added to the query. If not, it will just end up in a cross product. In that case, when loading participations and users, many rows are returned, one for every user. If the participation's user isn't in the identity map, a new one will be created with its fields initialized using the data of the first result row, even if it's for another user.
1 parent d7c90a9 commit 5ec9f21

2 files changed

Lines changed: 29 additions & 2 deletions

File tree

cms/server/contest/authentication.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
import logging
3939
from datetime import timedelta
4040

41-
from sqlalchemy.orm import contains_eager
41+
from sqlalchemy.orm import contains_eager, joinedload
4242

4343
from cms import config
4444
from cms.db import Participation, User
@@ -259,7 +259,7 @@ def _authenticate_request_by_ip_address(sql_session, contest, ip_address):
259259
ip_network = ipaddress.ip_network((ip_address, ip_address.max_prefixlen))
260260

261261
participations = sql_session.query(Participation) \
262-
.options(contains_eager(Participation.user)) \
262+
.options(joinedload(Participation.user)) \
263263
.filter(Participation.contest == contest) \
264264
.filter(Participation.ip.any(ip_network))
265265

cmstestsuite/unit_tests/server/contest/authentication_test.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,32 @@ class TestValidateLogin(DatabaseMixin, unittest.TestCase):
5050
def setUp(self):
5151
super(TestValidateLogin, self).setUp()
5252
self.timestamp = make_datetime()
53+
self.add_contest()
5354
self.contest = self.add_contest(allow_password_authentication=True)
55+
self.add_user(username="otheruser")
5456
self.user = self.add_user(
5557
username="myuser", password=build_password("mypass"))
5658
self.participation = self.add_participation(
5759
contest=self.contest, user=self.user)
5860

5961
def assertSuccess(self, username, password, ip_address):
62+
# We had an issue where due to a misuse of contains_eager we ended up
63+
# with the wrong user attached to the participation. This only happens
64+
# if the correct user isn't already in the identity map, which is what
65+
# these lines trigger.
66+
self.session.flush()
67+
self.session.expire(self.user)
68+
self.session.expire(self.contest)
69+
6070
authenticated_participation, cookie = validate_login(
6171
self.session, self.contest, self.timestamp,
6272
username, password, ipaddress.ip_address(ip_address))
6373

6474
self.assertIsNotNone(authenticated_participation)
6575
self.assertIsNotNone(cookie)
6676
self.assertIs(authenticated_participation, self.participation)
77+
self.assertIs(authenticated_participation.user, self.user)
78+
self.assertIs(authenticated_participation.contest, self.contest)
6779

6880
def assertFailure(self, username, password, ip_address):
6981
authenticated_participation, cookie = validate_login(
@@ -152,7 +164,9 @@ class TestAuthenticateRequest(DatabaseMixin, unittest.TestCase):
152164
def setUp(self):
153165
super(TestAuthenticateRequest, self).setUp()
154166
self.timestamp = make_datetime()
167+
self.add_contest()
155168
self.contest = self.add_contest()
169+
self.add_user(username="otheruser")
156170
self.user = self.add_user(
157171
username="myuser", password=build_password("mypass"))
158172
self.participation = self.add_participation(
@@ -176,10 +190,23 @@ def assertSuccess(self, **kwargs):
176190
# autologin or thanks to the cookie) and return the cookie that should
177191
# be set (or None, if it should be cleared/left unset).
178192
# The arguments are the same as those of attempt_authentication.
193+
194+
# We had an issue where due to a misuse of contains_eager we ended up
195+
# with the wrong user attached to the participation. This only happens
196+
# if the correct user isn't already in the identity map, which is what
197+
# these lines trigger.
198+
self.session.flush()
199+
self.session.expire(self.user)
200+
self.session.expire(self.contest)
201+
179202
authenticated_participation, cookie = \
180203
self.attempt_authentication(**kwargs)
204+
181205
self.assertIsNotNone(authenticated_participation)
182206
self.assertIs(authenticated_participation, self.participation)
207+
self.assertIs(authenticated_participation.user, self.user)
208+
self.assertIs(authenticated_participation.contest, self.contest)
209+
183210
return cookie
184211

185212
def assertSuccessAndCookieRefreshed(self, **kwargs):

0 commit comments

Comments
 (0)