Skip to content

Security: Fix checking of admin tokens in CWS#1696

Merged
gollux merged 1 commit into
cms-dev:mainfrom
gollux:api-auth-fix
May 13, 2026
Merged

Security: Fix checking of admin tokens in CWS#1696
gollux merged 1 commit into
cms-dev:mainfrom
gollux:api-auth-fix

Conversation

@gollux
Copy link
Copy Markdown
Contributor

@gollux gollux commented May 13, 2026

When the login endpoint of the CWS API was called with an admin token, but no admin token was configured, login succeeded.

This allowed attackers to impersonate any CWS user in CMS instances with no admin token configured.

Thanks to Samet Akıllı ssametakilli@gmail.com for reporting the issue.

When the login endpoint of the CWS API was called with an admin token,
but no admin token was configured, login succeeded.

This allowed attackers to impersonate any CWS user in CMS instances with
no admin token configured.

Thanks to Samet Akıllı <ssametakilli@gmail.com> for reporting the issue.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

❌ 22 Tests Failed:

Tests completed Failed Passed Skipped
697 22 675 8
View the top 3 failed test(s) by shortest run time
cmstestsuite/unit_tests/server/contest/authentication_test.py::TestValidateLogin::test_impersonation_overrides_unallowed_password_authentication
Stack Traces | 0.011s run time
self = <cmstestsuite.unit_tests.server.contest.authentication_test.TestValidateLogin testMethod=test_impersonation_overrides_unallowed_password_authentication>

    def test_impersonation_overrides_unallowed_password_authentication(self):
        self.contest.allow_password_authentication = False
    
>       self.assertSuccess("myuser", "", "127.0.0.1", "admin-token")

.../server/contest/authentication_test.py:171: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../server/contest/authentication_test.py:69: in assertSuccess
    authenticated_participation, cookie = validate_login(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

sql_session = <sqlalchemy.orm.session.Session object at 0x7f30be43b010>
contest = <cms.db.contest.Contest object at 0x7f30be438210>
timestamp = datetime.datetime(2026, 5, 13, 14, 44, 38, 101796)
username = 'myuser', password = '', ip_address = IPv4Address('127.0.0.1')
admin_token = 'admin-token'

    def validate_login(
        sql_session: Session,
        contest: Contest,
        timestamp: datetime,
        username: str,
        password: str,
        ip_address: AnyIPAddress,
        admin_token: str = ""
    ) -> tuple[Participation | None, bytes | None]:
        """Authenticate a user logging in, with username and password.
    
        Given the information the user provided (the username and the
        password) and some context information (contest, to determine which
        users are allowed to log in, how and with which restrictions;
        timestamp for cookie creation; IP address to check against) try to
        authenticate the user and return its participation and the cookie
        to set to help authenticate future visits.
    
        After finding the participation, IP login and hidden users
        restrictions are checked.
    
        sql_session: the SQLAlchemy database session used to
            execute queries.
        contest: the contest the user is trying to access.
        timestamp: the date and the time of the request.
        username: the username the user provided.
        password: the password the user provided.
        ip_address: the IP address the request came from.
        admin_token: administrator's token used to impersonate a user
    
        return: if the user couldn't
            be authenticated then return None, otherwise return the
            participation that they wanted to authenticate as; if a cookie
            has to be set return it as well, otherwise return None.
    
        """
        def log_failed_attempt(msg, *args):
            logger.info("Unsuccessful login attempt from IP address %s, as user "
                        "%r, on contest %s, at %s: " + msg, ip_address,
                        username, contest.name, timestamp, *args)
    
        if not contest.allow_password_authentication and admin_token == "":
            log_failed_attempt("password authentication not allowed")
            return None, None
    
        participation: Participation | None = (
            sql_session.query(Participation)
            .join(Participation.user)
            .options(contains_eager(Participation.user))
            .filter(Participation.contest == contest)
            .filter(User.username == username)
            .first()
        )
    
        if participation is None:
            log_failed_attempt("user not registered to contest")
            return None, None
    
        if admin_token != "":
>           if config.contest_admin_token is None:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'Config' object has no attribute 'contest_admin_token'

.../server/contest/authentication.py:126: AttributeError
cmstestsuite/unit_tests/server/contest/authentication_test.py::TestValidateLogin::test_successful_impersonation
Stack Traces | 0.011s run time
self = <cmstestsuite.unit_tests.server.contest.authentication_test.TestValidateLogin testMethod=test_successful_impersonation>

    def test_successful_impersonation(self):
>       self.assertSuccess("myuser", "", "127.0.0.1", "admin-token")

.../server/contest/authentication_test.py:163: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../server/contest/authentication_test.py:69: in assertSuccess
    authenticated_participation, cookie = validate_login(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

sql_session = <sqlalchemy.orm.session.Session object at 0x7f30bfffffd0>
contest = <cms.db.contest.Contest object at 0x7f30be592c90>
timestamp = datetime.datetime(2026, 5, 13, 14, 44, 38, 232147)
username = 'myuser', password = '', ip_address = IPv4Address('127.0.0.1')
admin_token = 'admin-token'

    def validate_login(
        sql_session: Session,
        contest: Contest,
        timestamp: datetime,
        username: str,
        password: str,
        ip_address: AnyIPAddress,
        admin_token: str = ""
    ) -> tuple[Participation | None, bytes | None]:
        """Authenticate a user logging in, with username and password.
    
        Given the information the user provided (the username and the
        password) and some context information (contest, to determine which
        users are allowed to log in, how and with which restrictions;
        timestamp for cookie creation; IP address to check against) try to
        authenticate the user and return its participation and the cookie
        to set to help authenticate future visits.
    
        After finding the participation, IP login and hidden users
        restrictions are checked.
    
        sql_session: the SQLAlchemy database session used to
            execute queries.
        contest: the contest the user is trying to access.
        timestamp: the date and the time of the request.
        username: the username the user provided.
        password: the password the user provided.
        ip_address: the IP address the request came from.
        admin_token: administrator's token used to impersonate a user
    
        return: if the user couldn't
            be authenticated then return None, otherwise return the
            participation that they wanted to authenticate as; if a cookie
            has to be set return it as well, otherwise return None.
    
        """
        def log_failed_attempt(msg, *args):
            logger.info("Unsuccessful login attempt from IP address %s, as user "
                        "%r, on contest %s, at %s: " + msg, ip_address,
                        username, contest.name, timestamp, *args)
    
        if not contest.allow_password_authentication and admin_token == "":
            log_failed_attempt("password authentication not allowed")
            return None, None
    
        participation: Participation | None = (
            sql_session.query(Participation)
            .join(Participation.user)
            .options(contains_eager(Participation.user))
            .filter(Participation.contest == contest)
            .filter(User.username == username)
            .first()
        )
    
        if participation is None:
            log_failed_attempt("user not registered to contest")
            return None, None
    
        if admin_token != "":
>           if config.contest_admin_token is None:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'Config' object has no attribute 'contest_admin_token'

.../server/contest/authentication.py:126: AttributeError
cmstestsuite/unit_tests/server/contest/authentication_test.py::TestValidateLogin::test_impersonation_overrides_ip_lock
Stack Traces | 0.013s run time
self = <cmstestsuite.unit_tests.server.contest.authentication_test.TestValidateLogin testMethod=test_impersonation_overrides_ip_lock>

    def test_impersonation_overrides_ip_lock(self):
        self.contest.ip_restriction = True
        self.participation.ip = [ipaddress.ip_network("10.0.0.0/24")]
    
>       self.assertSuccess("myuser", "mypass", "10.0.0.1", "admin-token")

.../server/contest/authentication_test.py:183: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../server/contest/authentication_test.py:69: in assertSuccess
    authenticated_participation, cookie = validate_login(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

sql_session = <sqlalchemy.orm.session.Session object at 0x7f30bddeee90>
contest = <cms.db.contest.Contest object at 0x7f30be3dc750>
timestamp = datetime.datetime(2026, 5, 13, 14, 44, 37, 972099)
username = 'myuser', password = 'mypass', ip_address = IPv4Address('10.0.0.1')
admin_token = 'admin-token'

    def validate_login(
        sql_session: Session,
        contest: Contest,
        timestamp: datetime,
        username: str,
        password: str,
        ip_address: AnyIPAddress,
        admin_token: str = ""
    ) -> tuple[Participation | None, bytes | None]:
        """Authenticate a user logging in, with username and password.
    
        Given the information the user provided (the username and the
        password) and some context information (contest, to determine which
        users are allowed to log in, how and with which restrictions;
        timestamp for cookie creation; IP address to check against) try to
        authenticate the user and return its participation and the cookie
        to set to help authenticate future visits.
    
        After finding the participation, IP login and hidden users
        restrictions are checked.
    
        sql_session: the SQLAlchemy database session used to
            execute queries.
        contest: the contest the user is trying to access.
        timestamp: the date and the time of the request.
        username: the username the user provided.
        password: the password the user provided.
        ip_address: the IP address the request came from.
        admin_token: administrator's token used to impersonate a user
    
        return: if the user couldn't
            be authenticated then return None, otherwise return the
            participation that they wanted to authenticate as; if a cookie
            has to be set return it as well, otherwise return None.
    
        """
        def log_failed_attempt(msg, *args):
            logger.info("Unsuccessful login attempt from IP address %s, as user "
                        "%r, on contest %s, at %s: " + msg, ip_address,
                        username, contest.name, timestamp, *args)
    
        if not contest.allow_password_authentication and admin_token == "":
            log_failed_attempt("password authentication not allowed")
            return None, None
    
        participation: Participation | None = (
            sql_session.query(Participation)
            .join(Participation.user)
            .options(contains_eager(Participation.user))
            .filter(Participation.contest == contest)
            .filter(User.username == username)
            .first()
        )
    
        if participation is None:
            log_failed_attempt("user not registered to contest")
            return None, None
    
        if admin_token != "":
>           if config.contest_admin_token is None:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'Config' object has no attribute 'contest_admin_token'

.../server/contest/authentication.py:126: AttributeError
cmstestsuite/unit_tests/server/contest/authentication_test.py::TestValidateLogin::test_impersonation_overrides_unallowed_hidden_participation
Stack Traces | 0.014s run time
self = <cmstestsuite.unit_tests.server.contest.authentication_test.TestValidateLogin testMethod=test_impersonation_overrides_unallowed_hidden_participation>

    def test_impersonation_overrides_unallowed_hidden_participation(self):
        self.contest.block_hidden_participations = True
        self.participation.hidden = True
    
>       self.assertSuccess("myuser", "", "127.0.0.1", "admin-token")

.../server/contest/authentication_test.py:177: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../server/contest/authentication_test.py:69: in assertSuccess
    authenticated_participation, cookie = validate_login(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

sql_session = <sqlalchemy.orm.session.Session object at 0x7f30bdb2ccd0>
contest = <cms.db.contest.Contest object at 0x7f30bdb2dfd0>
timestamp = datetime.datetime(2026, 5, 13, 14, 44, 38, 67038)
username = 'myuser', password = '', ip_address = IPv4Address('127.0.0.1')
admin_token = 'admin-token'

    def validate_login(
        sql_session: Session,
        contest: Contest,
        timestamp: datetime,
        username: str,
        password: str,
        ip_address: AnyIPAddress,
        admin_token: str = ""
    ) -> tuple[Participation | None, bytes | None]:
        """Authenticate a user logging in, with username and password.
    
        Given the information the user provided (the username and the
        password) and some context information (contest, to determine which
        users are allowed to log in, how and with which restrictions;
        timestamp for cookie creation; IP address to check against) try to
        authenticate the user and return its participation and the cookie
        to set to help authenticate future visits.
    
        After finding the participation, IP login and hidden users
        restrictions are checked.
    
        sql_session: the SQLAlchemy database session used to
            execute queries.
        contest: the contest the user is trying to access.
        timestamp: the date and the time of the request.
        username: the username the user provided.
        password: the password the user provided.
        ip_address: the IP address the request came from.
        admin_token: administrator's token used to impersonate a user
    
        return: if the user couldn't
            be authenticated then return None, otherwise return the
            participation that they wanted to authenticate as; if a cookie
            has to be set return it as well, otherwise return None.
    
        """
        def log_failed_attempt(msg, *args):
            logger.info("Unsuccessful login attempt from IP address %s, as user "
                        "%r, on contest %s, at %s: " + msg, ip_address,
                        username, contest.name, timestamp, *args)
    
        if not contest.allow_password_authentication and admin_token == "":
            log_failed_attempt("password authentication not allowed")
            return None, None
    
        participation: Participation | None = (
            sql_session.query(Participation)
            .join(Participation.user)
            .options(contains_eager(Participation.user))
            .filter(Participation.contest == contest)
            .filter(User.username == username)
            .first()
        )
    
        if participation is None:
            log_failed_attempt("user not registered to contest")
            return None, None
    
        if admin_token != "":
>           if config.contest_admin_token is None:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'Config' object has no attribute 'contest_admin_token'

.../server/contest/authentication.py:126: AttributeError
cmstestsuite/unit_tests/server/contest/authentication_test.py::TestValidateLogin::test_unsuccessful_impersonation
Stack Traces | 0.055s run time
self = <cmstestsuite.unit_tests.server.contest.authentication_test.TestValidateLogin testMethod=test_unsuccessful_impersonation>

    def test_unsuccessful_impersonation(self):
>       self.assertFailure("myuser", "", "127.0.0.1", "bad-admin-token")

.../server/contest/authentication_test.py:166: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../server/contest/authentication_test.py:81: in assertFailure
    authenticated_participation, cookie = validate_login(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

sql_session = <sqlalchemy.orm.session.Session object at 0x7f30be438a10>
contest = <cms.db.contest.Contest object at 0x7f30be268c90>
timestamp = datetime.datetime(2026, 5, 13, 14, 44, 38, 293262)
username = 'myuser', password = '', ip_address = IPv4Address('127.0.0.1')
admin_token = 'bad-admin-token'

    def validate_login(
        sql_session: Session,
        contest: Contest,
        timestamp: datetime,
        username: str,
        password: str,
        ip_address: AnyIPAddress,
        admin_token: str = ""
    ) -> tuple[Participation | None, bytes | None]:
        """Authenticate a user logging in, with username and password.
    
        Given the information the user provided (the username and the
        password) and some context information (contest, to determine which
        users are allowed to log in, how and with which restrictions;
        timestamp for cookie creation; IP address to check against) try to
        authenticate the user and return its participation and the cookie
        to set to help authenticate future visits.
    
        After finding the participation, IP login and hidden users
        restrictions are checked.
    
        sql_session: the SQLAlchemy database session used to
            execute queries.
        contest: the contest the user is trying to access.
        timestamp: the date and the time of the request.
        username: the username the user provided.
        password: the password the user provided.
        ip_address: the IP address the request came from.
        admin_token: administrator's token used to impersonate a user
    
        return: if the user couldn't
            be authenticated then return None, otherwise return the
            participation that they wanted to authenticate as; if a cookie
            has to be set return it as well, otherwise return None.
    
        """
        def log_failed_attempt(msg, *args):
            logger.info("Unsuccessful login attempt from IP address %s, as user "
                        "%r, on contest %s, at %s: " + msg, ip_address,
                        username, contest.name, timestamp, *args)
    
        if not contest.allow_password_authentication and admin_token == "":
            log_failed_attempt("password authentication not allowed")
            return None, None
    
        participation: Participation | None = (
            sql_session.query(Participation)
            .join(Participation.user)
            .options(contains_eager(Participation.user))
            .filter(Participation.contest == contest)
            .filter(User.username == username)
            .first()
        )
    
        if participation is None:
            log_failed_attempt("user not registered to contest")
            return None, None
    
        if admin_token != "":
>           if config.contest_admin_token is None:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'Config' object has no attribute 'contest_admin_token'

.../server/contest/authentication.py:126: AttributeError
cmstestsuite/unit_tests/server/contest/authentication_test.py::TestAuthenticateRequest::test_authorization_header
Stack Traces | 0.208s run time
self = <cmstestsuite.unit_tests.server.contest.authentication_test.TestAuthenticateRequest testMethod=test_authorization_header>

    def setUp(self):
        super().setUp()
        self.timestamp = make_datetime()
        self.add_contest()
        self.contest = self.add_contest()
        self.user = self.add_user(
            username="myuser", password=build_password("mypass"))
        self.participation = self.add_participation(
            contest=self.contest, user=self.user)
        _, self.cookie = validate_login(
            self.session, self.contest, self.timestamp, self.user.username,
            "mypass", ipaddress.ip_address("10.0.0.1"))
    
        # For testing impersonation by admin token
        self.impersonated_user = self.add_user(username="otheruser")
        self.impersonated_participation = self.add_participation(
            contest=self.contest, user=self.impersonated_user)
        with patch.object(
            config.contest_web_server, "contest_admin_token", "admin-token"
        ):
>           _, self.impersonated_cookie = validate_login(
                self.session, self.contest, self.timestamp, "otheruser",
                "", ipaddress.ip_address("10.0.0.2"), "admin-token")

.../server/contest/authentication_test.py:209: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

sql_session = <sqlalchemy.orm.session.Session object at 0x7f30be39c510>
contest = <cms.db.contest.Contest object at 0x7f30be420fd0>
timestamp = datetime.datetime(2026, 5, 13, 14, 44, 38, 559438)
username = 'otheruser', password = '', ip_address = IPv4Address('10.0.0.2')
admin_token = 'admin-token'

    def validate_login(
        sql_session: Session,
        contest: Contest,
        timestamp: datetime,
        username: str,
        password: str,
        ip_address: AnyIPAddress,
        admin_token: str = ""
    ) -> tuple[Participation | None, bytes | None]:
        """Authenticate a user logging in, with username and password.
    
        Given the information the user provided (the username and the
        password) and some context information (contest, to determine which
        users are allowed to log in, how and with which restrictions;
        timestamp for cookie creation; IP address to check against) try to
        authenticate the user and return its participation and the cookie
        to set to help authenticate future visits.
    
        After finding the participation, IP login and hidden users
        restrictions are checked.
    
        sql_session: the SQLAlchemy database session used to
            execute queries.
        contest: the contest the user is trying to access.
        timestamp: the date and the time of the request.
        username: the username the user provided.
        password: the password the user provided.
        ip_address: the IP address the request came from.
        admin_token: administrator's token used to impersonate a user
    
        return: if the user couldn't
            be authenticated then return None, otherwise return the
            participation that they wanted to authenticate as; if a cookie
            has to be set return it as well, otherwise return None.
    
        """
        def log_failed_attempt(msg, *args):
            logger.info("Unsuccessful login attempt from IP address %s, as user "
                        "%r, on contest %s, at %s: " + msg, ip_address,
                        username, contest.name, timestamp, *args)
    
        if not contest.allow_password_authentication and admin_token == "":
            log_failed_attempt("password authentication not allowed")
            return None, None
    
        participation: Participation | None = (
            sql_session.query(Participation)
            .join(Participation.user)
            .options(contains_eager(Participation.user))
            .filter(Participation.contest == contest)
            .filter(User.username == username)
            .first()
        )
    
        if participation is None:
            log_failed_attempt("user not registered to contest")
            return None, None
    
        if admin_token != "":
>           if config.contest_admin_token is None:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'Config' object has no attribute 'contest_admin_token'

.../server/contest/authentication.py:126: AttributeError
cmstestsuite/unit_tests/server/contest/authentication_test.py::TestAuthenticateRequest::test_cookie_contains_password
Stack Traces | 51.9s run time
self = <cmstestsuite.unit_tests.server.contest.authentication_test.TestAuthenticateRequest testMethod=test_cookie_contains_password>

    def setUp(self):
        super().setUp()
        self.timestamp = make_datetime()
        self.add_contest()
        self.contest = self.add_contest()
        self.user = self.add_user(
            username="myuser", password=build_password("mypass"))
        self.participation = self.add_participation(
            contest=self.contest, user=self.user)
        _, self.cookie = validate_login(
            self.session, self.contest, self.timestamp, self.user.username,
            "mypass", ipaddress.ip_address("10.0.0.1"))
    
        # For testing impersonation by admin token
        self.impersonated_user = self.add_user(username="otheruser")
        self.impersonated_participation = self.add_participation(
            contest=self.contest, user=self.impersonated_user)
        with patch.object(
            config.contest_web_server, "contest_admin_token", "admin-token"
        ):
>           _, self.impersonated_cookie = validate_login(
                self.session, self.contest, self.timestamp, "otheruser",
                "", ipaddress.ip_address("10.0.0.2"), "admin-token")

.../server/contest/authentication_test.py:209: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

sql_session = <sqlalchemy.orm.session.Session object at 0x7f30be753790>
contest = <cms.db.contest.Contest object at 0x7f30bdd9b210>
timestamp = datetime.datetime(2026, 5, 13, 14, 44, 38, 595384)
username = 'otheruser', password = '', ip_address = IPv4Address('10.0.0.2')
admin_token = 'admin-token'

    def validate_login(
        sql_session: Session,
        contest: Contest,
        timestamp: datetime,
        username: str,
        password: str,
        ip_address: AnyIPAddress,
        admin_token: str = ""
    ) -> tuple[Participation | None, bytes | None]:
        """Authenticate a user logging in, with username and password.
    
        Given the information the user provided (the username and the
        password) and some context information (contest, to determine which
        users are allowed to log in, how and with which restrictions;
        timestamp for cookie creation; IP address to check against) try to
        authenticate the user and return its participation and the cookie
        to set to help authenticate future visits.
    
        After finding the participation, IP login and hidden users
        restrictions are checked.
    
        sql_session: the SQLAlchemy database session used to
            execute queries.
        contest: the contest the user is trying to access.
        timestamp: the date and the time of the request.
        username: the username the user provided.
        password: the password the user provided.
        ip_address: the IP address the request came from.
        admin_token: administrator's token used to impersonate a user
    
        return: if the user couldn't
            be authenticated then return None, otherwise return the
            participation that they wanted to authenticate as; if a cookie
            has to be set return it as well, otherwise return None.
    
        """
        def log_failed_attempt(msg, *args):
            logger.info("Unsuccessful login attempt from IP address %s, as user "
                        "%r, on contest %s, at %s: " + msg, ip_address,
                        username, contest.name, timestamp, *args)
    
        if not contest.allow_password_authentication and admin_token == "":
            log_failed_attempt("password authentication not allowed")
            return None, None
    
        participation: Participation | None = (
            sql_session.query(Participation)
            .join(Participation.user)
            .options(contains_eager(Participation.user))
            .filter(Participation.contest == contest)
            .filter(User.username == username)
            .first()
        )
    
        if participation is None:
            log_failed_attempt("user not registered to contest")
            return None, None
    
        if admin_token != "":
>           if config.contest_admin_token is None:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'Config' object has no attribute 'contest_admin_token'

.../server/contest/authentication.py:126: AttributeError
cmstestsuite/unit_tests/server/contest/authentication_test.py::TestAuthenticateRequest::test_cookie_contains_timestamp
Stack Traces | 217s run time
self = <cmstestsuite.unit_tests.server.contest.authentication_test.TestAuthenticateRequest testMethod=test_cookie_contains_timestamp>

    def setUp(self):
        super().setUp()
        self.timestamp = make_datetime()
        self.add_contest()
        self.contest = self.add_contest()
        self.user = self.add_user(
            username="myuser", password=build_password("mypass"))
        self.participation = self.add_participation(
            contest=self.contest, user=self.user)
        _, self.cookie = validate_login(
            self.session, self.contest, self.timestamp, self.user.username,
            "mypass", ipaddress.ip_address("10.0.0.1"))
    
        # For testing impersonation by admin token
        self.impersonated_user = self.add_user(username="otheruser")
        self.impersonated_participation = self.add_participation(
            contest=self.contest, user=self.impersonated_user)
        with patch.object(
            config.contest_web_server, "contest_admin_token", "admin-token"
        ):
>           _, self.impersonated_cookie = validate_login(
                self.session, self.contest, self.timestamp, "otheruser",
                "", ipaddress.ip_address("10.0.0.2"), "admin-token")

.../server/contest/authentication_test.py:209: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

sql_session = <sqlalchemy.orm.session.Session object at 0x7f30be5aae10>
contest = <cms.db.contest.Contest object at 0x7f30be4d8850>
timestamp = datetime.datetime(2026, 5, 13, 14, 45, 30, 496514)
username = 'otheruser', password = '', ip_address = IPv4Address('10.0.0.2')
admin_token = 'admin-token'

    def validate_login(
        sql_session: Session,
        contest: Contest,
        timestamp: datetime,
        username: str,
        password: str,
        ip_address: AnyIPAddress,
        admin_token: str = ""
    ) -> tuple[Participation | None, bytes | None]:
        """Authenticate a user logging in, with username and password.
    
        Given the information the user provided (the username and the
        password) and some context information (contest, to determine which
        users are allowed to log in, how and with which restrictions;
        timestamp for cookie creation; IP address to check against) try to
        authenticate the user and return its participation and the cookie
        to set to help authenticate future visits.
    
        After finding the participation, IP login and hidden users
        restrictions are checked.
    
        sql_session: the SQLAlchemy database session used to
            execute queries.
        contest: the contest the user is trying to access.
        timestamp: the date and the time of the request.
        username: the username the user provided.
        password: the password the user provided.
        ip_address: the IP address the request came from.
        admin_token: administrator's token used to impersonate a user
    
        return: if the user couldn't
            be authenticated then return None, otherwise return the
            participation that they wanted to authenticate as; if a cookie
            has to be set return it as well, otherwise return None.
    
        """
        def log_failed_attempt(msg, *args):
            logger.info("Unsuccessful login attempt from IP address %s, as user "
                        "%r, on contest %s, at %s: " + msg, ip_address,
                        username, contest.name, timestamp, *args)
    
        if not contest.allow_password_authentication and admin_token == "":
            log_failed_attempt("password authentication not allowed")
            return None, None
    
        participation: Participation | None = (
            sql_session.query(Participation)
            .join(Participation.user)
            .options(contains_eager(Participation.user))
            .filter(Participation.contest == contest)
            .filter(User.username == username)
            .first()
        )
    
        if participation is None:
            log_failed_attempt("user not registered to contest")
            return None, None
    
        if admin_token != "":
>           if config.contest_admin_token is None:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'Config' object has no attribute 'contest_admin_token'

.../server/contest/authentication.py:126: AttributeError
cmstestsuite/unit_tests/server/contest/authentication_test.py::TestAuthenticateRequest::test_impersonation_overrides_unallowed_hidden_participation
Stack Traces | 222s run time
self = <cmstestsuite.unit_tests.server.contest.authentication_test.TestAuthenticateRequest testMethod=test_impersonation_overrides_unallowed_hidden_participation>

    def setUp(self):
        super().setUp()
        self.timestamp = make_datetime()
        self.add_contest()
        self.contest = self.add_contest()
        self.user = self.add_user(
            username="myuser", password=build_password("mypass"))
        self.participation = self.add_participation(
            contest=self.contest, user=self.user)
        _, self.cookie = validate_login(
            self.session, self.contest, self.timestamp, self.user.username,
            "mypass", ipaddress.ip_address("10.0.0.1"))
    
        # For testing impersonation by admin token
        self.impersonated_user = self.add_user(username="otheruser")
        self.impersonated_participation = self.add_participation(
            contest=self.contest, user=self.impersonated_user)
        with patch.object(
            config.contest_web_server, "contest_admin_token", "admin-token"
        ):
>           _, self.impersonated_cookie = validate_login(
                self.session, self.contest, self.timestamp, "otheruser",
                "", ipaddress.ip_address("10.0.0.2"), "admin-token")

.../server/contest/authentication_test.py:209: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

sql_session = <sqlalchemy.orm.session.Session object at 0x7f30bddfdc10>
contest = <cms.db.contest.Contest object at 0x7f30be43b490>
timestamp = datetime.datetime(2026, 5, 13, 15, 11, 21, 398095)
username = 'otheruser', password = '', ip_address = IPv4Address('10.0.0.2')
admin_token = 'admin-token'

    def validate_login(
        sql_session: Session,
        contest: Contest,
        timestamp: datetime,
        username: str,
        password: str,
        ip_address: AnyIPAddress,
        admin_token: str = ""
    ) -> tuple[Participation | None, bytes | None]:
        """Authenticate a user logging in, with username and password.
    
        Given the information the user provided (the username and the
        password) and some context information (contest, to determine which
        users are allowed to log in, how and with which restrictions;
        timestamp for cookie creation; IP address to check against) try to
        authenticate the user and return its participation and the cookie
        to set to help authenticate future visits.
    
        After finding the participation, IP login and hidden users
        restrictions are checked.
    
        sql_session: the SQLAlchemy database session used to
            execute queries.
        contest: the contest the user is trying to access.
        timestamp: the date and the time of the request.
        username: the username the user provided.
        password: the password the user provided.
        ip_address: the IP address the request came from.
        admin_token: administrator's token used to impersonate a user
    
        return: if the user couldn't
            be authenticated then return None, otherwise return the
            participation that they wanted to authenticate as; if a cookie
            has to be set return it as well, otherwise return None.
    
        """
        def log_failed_attempt(msg, *args):
            logger.info("Unsuccessful login attempt from IP address %s, as user "
                        "%r, on contest %s, at %s: " + msg, ip_address,
                        username, contest.name, timestamp, *args)
    
        if not contest.allow_password_authentication and admin_token == "":
            log_failed_attempt("password authentication not allowed")
            return None, None
    
        participation: Participation | None = (
            sql_session.query(Participation)
            .join(Participation.user)
            .options(contains_eager(Participation.user))
            .filter(Participation.contest == contest)
            .filter(User.username == username)
            .first()
        )
    
        if participation is None:
            log_failed_attempt("user not registered to contest")
            return None, None
    
        if admin_token != "":
>           if config.contest_admin_token is None:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'Config' object has no attribute 'contest_admin_token'

.../server/contest/authentication.py:126: AttributeError
cmstestsuite/unit_tests/server/contest/authentication_test.py::TestAuthenticateRequest::test_invalid_cookie
Stack Traces | 222s run time
self = <cmstestsuite.unit_tests.server.contest.authentication_test.TestAuthenticateRequest testMethod=test_invalid_cookie>

    def setUp(self):
        super().setUp()
        self.timestamp = make_datetime()
        self.add_contest()
        self.contest = self.add_contest()
        self.user = self.add_user(
            username="myuser", password=build_password("mypass"))
        self.participation = self.add_participation(
            contest=self.contest, user=self.user)
        _, self.cookie = validate_login(
            self.session, self.contest, self.timestamp, self.user.username,
            "mypass", ipaddress.ip_address("10.0.0.1"))
    
        # For testing impersonation by admin token
        self.impersonated_user = self.add_user(username="otheruser")
        self.impersonated_participation = self.add_participation(
            contest=self.contest, user=self.impersonated_user)
        with patch.object(
            config.contest_web_server, "contest_admin_token", "admin-token"
        ):
>           _, self.impersonated_cookie = validate_login(
                self.session, self.contest, self.timestamp, "otheruser",
                "", ipaddress.ip_address("10.0.0.2"), "admin-token")

.../server/contest/authentication_test.py:209: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

sql_session = <sqlalchemy.orm.session.Session object at 0x7f30be3f6d90>
contest = <cms.db.contest.Contest object at 0x7f30bc5c97d0>
timestamp = datetime.datetime(2026, 5, 13, 15, 15, 3, 229388)
username = 'otheruser', password = '', ip_address = IPv4Address('10.0.0.2')
admin_token = 'admin-token'

    def validate_login(
        sql_session: Session,
        contest: Contest,
        timestamp: datetime,
        username: str,
        password: str,
        ip_address: AnyIPAddress,
        admin_token: str = ""
    ) -> tuple[Participation | None, bytes | None]:
        """Authenticate a user logging in, with username and password.
    
        Given the information the user provided (the username and the
        password) and some context information (contest, to determine which
        users are allowed to log in, how and with which restrictions;
        timestamp for cookie creation; IP address to check against) try to
        authenticate the user and return its participation and the cookie
        to set to help authenticate future visits.
    
        After finding the participation, IP login and hidden users
        restrictions are checked.
    
        sql_session: the SQLAlchemy database session used to
            execute queries.
        contest: the contest the user is trying to access.
        timestamp: the date and the time of the request.
        username: the username the user provided.
        password: the password the user provided.
        ip_address: the IP address the request came from.
        admin_token: administrator's token used to impersonate a user
    
        return: if the user couldn't
            be authenticated then return None, otherwise return the
            participation that they wanted to authenticate as; if a cookie
            has to be set return it as well, otherwise return None.
    
        """
        def log_failed_attempt(msg, *args):
            logger.info("Unsuccessful login attempt from IP address %s, as user "
                        "%r, on contest %s, at %s: " + msg, ip_address,
                        username, contest.name, timestamp, *args)
    
        if not contest.allow_password_authentication and admin_token == "":
            log_failed_attempt("password authentication not allowed")
            return None, None
    
        participation: Participation | None = (
            sql_session.query(Participation)
            .join(Participation.user)
            .options(contains_eager(Participation.user))
            .filter(Participation.contest == contest)
            .filter(User.username == username)
            .first()
        )
    
        if participation is None:
            log_failed_attempt("user not registered to contest")
            return None, None
    
        if admin_token != "":
>           if config.contest_admin_token is None:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'Config' object has no attribute 'contest_admin_token'

.../server/contest/authentication.py:126: AttributeError
cmstestsuite/unit_tests/server/contest/authentication_test.py::TestAuthenticateRequest::test_impersonation_failure
Stack Traces | 222s run time
self = <cmstestsuite.unit_tests.server.contest.authentication_test.TestAuthenticateRequest testMethod=test_impersonation_failure>

    def setUp(self):
        super().setUp()
        self.timestamp = make_datetime()
        self.add_contest()
        self.contest = self.add_contest()
        self.user = self.add_user(
            username="myuser", password=build_password("mypass"))
        self.participation = self.add_participation(
            contest=self.contest, user=self.user)
        _, self.cookie = validate_login(
            self.session, self.contest, self.timestamp, self.user.username,
            "mypass", ipaddress.ip_address("10.0.0.1"))
    
        # For testing impersonation by admin token
        self.impersonated_user = self.add_user(username="otheruser")
        self.impersonated_participation = self.add_participation(
            contest=self.contest, user=self.impersonated_user)
        with patch.object(
            config.contest_web_server, "contest_admin_token", "admin-token"
        ):
>           _, self.impersonated_cookie = validate_login(
                self.session, self.contest, self.timestamp, "otheruser",
                "", ipaddress.ip_address("10.0.0.2"), "admin-token")

.../server/contest/authentication_test.py:209: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

sql_session = <sqlalchemy.orm.session.Session object at 0x7f30be52b810>
contest = <cms.db.contest.Contest object at 0x7f30be39f4d0>
timestamp = datetime.datetime(2026, 5, 13, 15, 0, 14, 948486)
username = 'otheruser', password = '', ip_address = IPv4Address('10.0.0.2')
admin_token = 'admin-token'

    def validate_login(
        sql_session: Session,
        contest: Contest,
        timestamp: datetime,
        username: str,
        password: str,
        ip_address: AnyIPAddress,
        admin_token: str = ""
    ) -> tuple[Participation | None, bytes | None]:
        """Authenticate a user logging in, with username and password.
    
        Given the information the user provided (the username and the
        password) and some context information (contest, to determine which
        users are allowed to log in, how and with which restrictions;
        timestamp for cookie creation; IP address to check against) try to
        authenticate the user and return its participation and the cookie
        to set to help authenticate future visits.
    
        After finding the participation, IP login and hidden users
        restrictions are checked.
    
        sql_session: the SQLAlchemy database session used to
            execute queries.
        contest: the contest the user is trying to access.
        timestamp: the date and the time of the request.
        username: the username the user provided.
        password: the password the user provided.
        ip_address: the IP address the request came from.
        admin_token: administrator's token used to impersonate a user
    
        return: if the user couldn't
            be authenticated then return None, otherwise return the
            participation that they wanted to authenticate as; if a cookie
            has to be set return it as well, otherwise return None.
    
        """
        def log_failed_attempt(msg, *args):
            logger.info("Unsuccessful login attempt from IP address %s, as user "
                        "%r, on contest %s, at %s: " + msg, ip_address,
                        username, contest.name, timestamp, *args)
    
        if not contest.allow_password_authentication and admin_token == "":
            log_failed_attempt("password authentication not allowed")
            return None, None
    
        participation: Participation | None = (
            sql_session.query(Participation)
            .join(Participation.user)
            .options(contains_eager(Participation.user))
            .filter(Participation.contest == contest)
            .filter(User.username == username)
            .first()
        )
    
        if participation is None:
            log_failed_attempt("user not registered to contest")
            return None, None
    
        if admin_token != "":
>           if config.contest_admin_token is None:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'Config' object has no attribute 'contest_admin_token'

.../server/contest/authentication.py:126: AttributeError
cmstestsuite/unit_tests/server/contest/authentication_test.py::TestAuthenticateRequest::test_impersonation_overrides_ip_lock
Stack Traces | 222s run time
self = <cmstestsuite.unit_tests.server.contest.authentication_test.TestAuthenticateRequest testMethod=test_impersonation_overrides_ip_lock>

    def setUp(self):
        super().setUp()
        self.timestamp = make_datetime()
        self.add_contest()
        self.contest = self.add_contest()
        self.user = self.add_user(
            username="myuser", password=build_password("mypass"))
        self.participation = self.add_participation(
            contest=self.contest, user=self.user)
        _, self.cookie = validate_login(
            self.session, self.contest, self.timestamp, self.user.username,
            "mypass", ipaddress.ip_address("10.0.0.1"))
    
        # For testing impersonation by admin token
        self.impersonated_user = self.add_user(username="otheruser")
        self.impersonated_participation = self.add_participation(
            contest=self.contest, user=self.impersonated_user)
        with patch.object(
            config.contest_web_server, "contest_admin_token", "admin-token"
        ):
>           _, self.impersonated_cookie = validate_login(
                self.session, self.contest, self.timestamp, "otheruser",
                "", ipaddress.ip_address("10.0.0.2"), "admin-token")

.../server/contest/authentication_test.py:209: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

sql_session = <sqlalchemy.orm.session.Session object at 0x7f30be43f650>
contest = <cms.db.contest.Contest object at 0x7f30bc8a2050>
timestamp = datetime.datetime(2026, 5, 13, 15, 7, 39, 242266)
username = 'otheruser', password = '', ip_address = IPv4Address('10.0.0.2')
admin_token = 'admin-token'

    def validate_login(
        sql_session: Session,
        contest: Contest,
        timestamp: datetime,
        username: str,
        password: str,
        ip_address: AnyIPAddress,
        admin_token: str = ""
    ) -> tuple[Participation | None, bytes | None]:
        """Authenticate a user logging in, with username and password.
    
        Given the information the user provided (the username and the
        password) and some context information (contest, to determine which
        users are allowed to log in, how and with which restrictions;
        timestamp for cookie creation; IP address to check against) try to
        authenticate the user and return its participation and the cookie
        to set to help authenticate future visits.
    
        After finding the participation, IP login and hidden users
        restrictions are checked.
    
        sql_session: the SQLAlchemy database session used to
            execute queries.
        contest: the contest the user is trying to access.
        timestamp: the date and the time of the request.
        username: the username the user provided.
        password: the password the user provided.
        ip_address: the IP address the request came from.
        admin_token: administrator's token used to impersonate a user
    
        return: if the user couldn't
            be authenticated then return None, otherwise return the
            participation that they wanted to authenticate as; if a cookie
            has to be set return it as well, otherwise return None.
    
        """
        def log_failed_attempt(msg, *args):
            logger.info("Unsuccessful login attempt from IP address %s, as user "
                        "%r, on contest %s, at %s: " + msg, ip_address,
                        username, contest.name, timestamp, *args)
    
        if not contest.allow_password_authentication and admin_token == "":
            log_failed_attempt("password authentication not allowed")
            return None, None
    
        participation: Participation | None = (
            sql_session.query(Participation)
            .join(Participation.user)
            .options(contains_eager(Participation.user))
            .filter(Participation.contest == contest)
            .filter(User.username == username)
            .first()
        )
    
        if participation is None:
            log_failed_attempt("user not registered to contest")
            return None, None
    
        if admin_token != "":
>           if config.contest_admin_token is None:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'Config' object has no attribute 'contest_admin_token'

.../server/contest/authentication.py:126: AttributeError
cmstestsuite/unit_tests/server/contest/authentication_test.py::TestAuthenticateRequest::test_impersonation_no_config
Stack Traces | 222s run time
self = <cmstestsuite.unit_tests.server.contest.authentication_test.TestAuthenticateRequest testMethod=test_impersonation_no_config>

    def setUp(self):
        super().setUp()
        self.timestamp = make_datetime()
        self.add_contest()
        self.contest = self.add_contest()
        self.user = self.add_user(
            username="myuser", password=build_password("mypass"))
        self.participation = self.add_participation(
            contest=self.contest, user=self.user)
        _, self.cookie = validate_login(
            self.session, self.contest, self.timestamp, self.user.username,
            "mypass", ipaddress.ip_address("10.0.0.1"))
    
        # For testing impersonation by admin token
        self.impersonated_user = self.add_user(username="otheruser")
        self.impersonated_participation = self.add_participation(
            contest=self.contest, user=self.impersonated_user)
        with patch.object(
            config.contest_web_server, "contest_admin_token", "admin-token"
        ):
>           _, self.impersonated_cookie = validate_login(
                self.session, self.contest, self.timestamp, "otheruser",
                "", ipaddress.ip_address("10.0.0.2"), "admin-token")

.../server/contest/authentication_test.py:209: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

sql_session = <sqlalchemy.orm.session.Session object at 0x7f30bddf8cd0>
contest = <cms.db.contest.Contest object at 0x7f30be3d0410>
timestamp = datetime.datetime(2026, 5, 13, 15, 3, 57, 35067)
username = 'otheruser', password = '', ip_address = IPv4Address('10.0.0.2')
admin_token = 'admin-token'

    def validate_login(
        sql_session: Session,
        contest: Contest,
        timestamp: datetime,
        username: str,
        password: str,
        ip_address: AnyIPAddress,
        admin_token: str = ""
    ) -> tuple[Participation | None, bytes | None]:
        """Authenticate a user logging in, with username and password.
    
        Given the information the user provided (the username and the
        password) and some context information (contest, to determine which
        users are allowed to log in, how and with which restrictions;
        timestamp for cookie creation; IP address to check against) try to
        authenticate the user and return its participation and the cookie
        to set to help authenticate future visits.
    
        After finding the participation, IP login and hidden users
        restrictions are checked.
    
        sql_session: the SQLAlchemy database session used to
            execute queries.
        contest: the contest the user is trying to access.
        timestamp: the date and the time of the request.
        username: the username the user provided.
        password: the password the user provided.
        ip_address: the IP address the request came from.
        admin_token: administrator's token used to impersonate a user
    
        return: if the user couldn't
            be authenticated then return None, otherwise return the
            participation that they wanted to authenticate as; if a cookie
            has to be set return it as well, otherwise return None.
    
        """
        def log_failed_attempt(msg, *args):
            logger.info("Unsuccessful login attempt from IP address %s, as user "
                        "%r, on contest %s, at %s: " + msg, ip_address,
                        username, contest.name, timestamp, *args)
    
        if not contest.allow_password_authentication and admin_token == "":
            log_failed_attempt("password authentication not allowed")
            return None, None
    
        participation: Participation | None = (
            sql_session.query(Participation)
            .join(Participation.user)
            .options(contains_eager(Participation.user))
            .filter(Participation.contest == contest)
            .filter(User.username == username)
            .first()
        )
    
        if participation is None:
            log_failed_attempt("user not registered to contest")
            return None, None
    
        if admin_token != "":
>           if config.contest_admin_token is None:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'Config' object has no attribute 'contest_admin_token'

.../server/contest/authentication.py:126: AttributeError
cmstestsuite/unit_tests/server/contest/authentication_test.py::TestAuthenticateRequest::test_impersonate_overridden_by_ip_autologin
Stack Traces | 222s run time
self = <cmstestsuite.unit_tests.server.contest.authentication_test.TestAuthenticateRequest testMethod=test_impersonate_overridden_by_ip_autologin>

    def setUp(self):
        super().setUp()
        self.timestamp = make_datetime()
        self.add_contest()
        self.contest = self.add_contest()
        self.user = self.add_user(
            username="myuser", password=build_password("mypass"))
        self.participation = self.add_participation(
            contest=self.contest, user=self.user)
        _, self.cookie = validate_login(
            self.session, self.contest, self.timestamp, self.user.username,
            "mypass", ipaddress.ip_address("10.0.0.1"))
    
        # For testing impersonation by admin token
        self.impersonated_user = self.add_user(username="otheruser")
        self.impersonated_participation = self.add_participation(
            contest=self.contest, user=self.impersonated_user)
        with patch.object(
            config.contest_web_server, "contest_admin_token", "admin-token"
        ):
>           _, self.impersonated_cookie = validate_login(
                self.session, self.contest, self.timestamp, "otheruser",
                "", ipaddress.ip_address("10.0.0.2"), "admin-token")

.../server/contest/authentication_test.py:209: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

sql_session = <sqlalchemy.orm.session.Session object at 0x7f30bfffd790>
contest = <cms.db.contest.Contest object at 0x7f30bc882650>
timestamp = datetime.datetime(2026, 5, 13, 14, 56, 32, 726945)
username = 'otheruser', password = '', ip_address = IPv4Address('10.0.0.2')
admin_token = 'admin-token'

    def validate_login(
        sql_session: Session,
        contest: Contest,
        timestamp: datetime,
        username: str,
        password: str,
        ip_address: AnyIPAddress,
        admin_token: str = ""
    ) -> tuple[Participation | None, bytes | None]:
        """Authenticate a user logging in, with username and password.
    
        Given the information the user provided (the username and the
        password) and some context information (contest, to determine which
        users are allowed to log in, how and with which restrictions;
        timestamp for cookie creation; IP address to check against) try to
        authenticate the user and return its participation and the cookie
        to set to help authenticate future visits.
    
        After finding the participation, IP login and hidden users
        restrictions are checked.
    
        sql_session: the SQLAlchemy database session used to
            execute queries.
        contest: the contest the user is trying to access.
        timestamp: the date and the time of the request.
        username: the username the user provided.
        password: the password the user provided.
        ip_address: the IP address the request came from.
        admin_token: administrator's token used to impersonate a user
    
        return: if the user couldn't
            be authenticated then return None, otherwise return the
            participation that they wanted to authenticate as; if a cookie
            has to be set return it as well, otherwise return None.
    
        """
        def log_failed_attempt(msg, *args):
            logger.info("Unsuccessful login attempt from IP address %s, as user "
                        "%r, on contest %s, at %s: " + msg, ip_address,
                        username, contest.name, timestamp, *args)
    
        if not contest.allow_password_authentication and admin_token == "":
            log_failed_attempt("password authentication not allowed")
            return None, None
    
        participation: Participation | None = (
            sql_session.query(Participation)
            .join(Participation.user)
            .options(contains_eager(Participation.user))
            .filter(Participation.contest == contest)
            .filter(User.username == username)
            .first()
        )
    
        if participation is None:
            log_failed_attempt("user not registered to contest")
            return None, None
    
        if admin_token != "":
>           if config.contest_admin_token is None:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'Config' object has no attribute 'contest_admin_token'

.../server/contest/authentication.py:126: AttributeError
cmstestsuite/unit_tests/server/contest/authentication_test.py::TestAuthenticateRequest::test_hidden_user
Stack Traces | 222s run time
self = <cmstestsuite.unit_tests.server.contest.authentication_test.TestAuthenticateRequest testMethod=test_hidden_user>

    def setUp(self):
        super().setUp()
        self.timestamp = make_datetime()
        self.add_contest()
        self.contest = self.add_contest()
        self.user = self.add_user(
            username="myuser", password=build_password("mypass"))
        self.participation = self.add_participation(
            contest=self.contest, user=self.user)
        _, self.cookie = validate_login(
            self.session, self.contest, self.timestamp, self.user.username,
            "mypass", ipaddress.ip_address("10.0.0.1"))
    
        # For testing impersonation by admin token
        self.impersonated_user = self.add_user(username="otheruser")
        self.impersonated_participation = self.add_participation(
            contest=self.contest, user=self.impersonated_user)
        with patch.object(
            config.contest_web_server, "contest_admin_token", "admin-token"
        ):
>           _, self.impersonated_cookie = validate_login(
                self.session, self.contest, self.timestamp, "otheruser",
                "", ipaddress.ip_address("10.0.0.2"), "admin-token")

.../server/contest/authentication_test.py:209: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

sql_session = <sqlalchemy.orm.session.Session object at 0x7f30bde3e250>
contest = <cms.db.contest.Contest object at 0x7f30c0200590>
timestamp = datetime.datetime(2026, 5, 13, 14, 49, 7, 845828)
username = 'otheruser', password = '', ip_address = IPv4Address('10.0.0.2')
admin_token = 'admin-token'

    def validate_login(
        sql_session: Session,
        contest: Contest,
        timestamp: datetime,
        username: str,
        password: str,
        ip_address: AnyIPAddress,
        admin_token: str = ""
    ) -> tuple[Participation | None, bytes | None]:
        """Authenticate a user logging in, with username and password.
    
        Given the information the user provided (the username and the
        password) and some context information (contest, to determine which
        users are allowed to log in, how and with which restrictions;
        timestamp for cookie creation; IP address to check against) try to
        authenticate the user and return its participation and the cookie
        to set to help authenticate future visits.
    
        After finding the participation, IP login and hidden users
        restrictions are checked.
    
        sql_session: the SQLAlchemy database session used to
            execute queries.
        contest: the contest the user is trying to access.
        timestamp: the date and the time of the request.
        username: the username the user provided.
        password: the password the user provided.
        ip_address: the IP address the request came from.
        admin_token: administrator's token used to impersonate a user
    
        return: if the user couldn't
            be authenticated then return None, otherwise return the
            participation that they wanted to authenticate as; if a cookie
            has to be set return it as well, otherwise return None.
    
        """
        def log_failed_attempt(msg, *args):
            logger.info("Unsuccessful login attempt from IP address %s, as user "
                        "%r, on contest %s, at %s: " + msg, ip_address,
                        username, contest.name, timestamp, *args)
    
        if not contest.allow_password_authentication and admin_token == "":
            log_failed_attempt("password authentication not allowed")
            return None, None
    
        participation: Participation | None = (
            sql_session.query(Participation)
            .join(Participation.user)
            .options(contains_eager(Participation.user))
            .filter(Participation.contest == contest)
            .filter(User.username == username)
            .first()
        )
    
        if participation is None:
            log_failed_attempt("user not registered to contest")
            return None, None
    
        if admin_token != "":
>           if config.contest_admin_token is None:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'Config' object has no attribute 'contest_admin_token'

.../server/contest/authentication.py:126: AttributeError
cmstestsuite/unit_tests/server/contest/authentication_test.py::TestAuthenticateRequest::test_impersonate
Stack Traces | 222s run time
self = <cmstestsuite.unit_tests.server.contest.authentication_test.TestAuthenticateRequest testMethod=test_impersonate>

    def setUp(self):
        super().setUp()
        self.timestamp = make_datetime()
        self.add_contest()
        self.contest = self.add_contest()
        self.user = self.add_user(
            username="myuser", password=build_password("mypass"))
        self.participation = self.add_participation(
            contest=self.contest, user=self.user)
        _, self.cookie = validate_login(
            self.session, self.contest, self.timestamp, self.user.username,
            "mypass", ipaddress.ip_address("10.0.0.1"))
    
        # For testing impersonation by admin token
        self.impersonated_user = self.add_user(username="otheruser")
        self.impersonated_participation = self.add_participation(
            contest=self.contest, user=self.impersonated_user)
        with patch.object(
            config.contest_web_server, "contest_admin_token", "admin-token"
        ):
>           _, self.impersonated_cookie = validate_login(
                self.session, self.contest, self.timestamp, "otheruser",
                "", ipaddress.ip_address("10.0.0.2"), "admin-token")

.../server/contest/authentication_test.py:209: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

sql_session = <sqlalchemy.orm.session.Session object at 0x7f30c00b8190>
contest = <cms.db.contest.Contest object at 0x7f30be370690>
timestamp = datetime.datetime(2026, 5, 13, 14, 52, 50, 241406)
username = 'otheruser', password = '', ip_address = IPv4Address('10.0.0.2')
admin_token = 'admin-token'

    def validate_login(
        sql_session: Session,
        contest: Contest,
        timestamp: datetime,
        username: str,
        password: str,
        ip_address: AnyIPAddress,
        admin_token: str = ""
    ) -> tuple[Participation | None, bytes | None]:
        """Authenticate a user logging in, with username and password.
    
        Given the information the user provided (the username and the
        password) and some context information (contest, to determine which
        users are allowed to log in, how and with which restrictions;
        timestamp for cookie creation; IP address to check against) try to
        authenticate the user and return its participation and the cookie
        to set to help authenticate future visits.
    
        After finding the participation, IP login and hidden users
        restrictions are checked.
    
        sql_session: the SQLAlchemy database session used to
            execute queries.
        contest: the contest the user is trying to access.
        timestamp: the date and the time of the request.
        username: the username the user provided.
        password: the password the user provided.
        ip_address: the IP address the request came from.
        admin_token: administrator's token used to impersonate a user
    
        return: if the user couldn't
            be authenticated then return None, otherwise return the
            participation that they wanted to authenticate as; if a cookie
            has to be set return it as well, otherwise return None.
    
        """
        def log_failed_attempt(msg, *args):
            logger.info("Unsuccessful login attempt from IP address %s, as user "
                        "%r, on contest %s, at %s: " + msg, ip_address,
                        username, contest.name, timestamp, *args)
    
        if not contest.allow_password_authentication and admin_token == "":
            log_failed_attempt("password authentication not allowed")
            return None, None
    
        participation: Participation | None = (
            sql_session.query(Participation)
            .join(Participation.user)
            .options(contains_eager(Participation.user))
            .filter(Participation.contest == contest)
            .filter(User.username == username)
            .first()
        )
    
        if participation is None:
            log_failed_attempt("user not registered to contest")
            return None, None
    
        if admin_token != "":
>           if config.contest_admin_token is None:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'Config' object has no attribute 'contest_admin_token'

.../server/contest/authentication.py:126: AttributeError
cmstestsuite/unit_tests/server/contest/authentication_test.py::TestAuthenticateRequest::test_invalid_password_in_database
Stack Traces | 223s run time
self = <cmstestsuite.unit_tests.server.contest.authentication_test.TestAuthenticateRequest testMethod=test_invalid_password_in_database>

    def setUp(self):
        super().setUp()
        self.timestamp = make_datetime()
        self.add_contest()
        self.contest = self.add_contest()
        self.user = self.add_user(
            username="myuser", password=build_password("mypass"))
        self.participation = self.add_participation(
            contest=self.contest, user=self.user)
        _, self.cookie = validate_login(
            self.session, self.contest, self.timestamp, self.user.username,
            "mypass", ipaddress.ip_address("10.0.0.1"))
    
        # For testing impersonation by admin token
        self.impersonated_user = self.add_user(username="otheruser")
        self.impersonated_participation = self.add_participation(
            contest=self.contest, user=self.impersonated_user)
        with patch.object(
            config.contest_web_server, "contest_admin_token", "admin-token"
        ):
>           _, self.impersonated_cookie = validate_login(
                self.session, self.contest, self.timestamp, "otheruser",
                "", ipaddress.ip_address("10.0.0.2"), "admin-token")

.../server/contest/authentication_test.py:209: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

sql_session = <sqlalchemy.orm.session.Session object at 0x7f30bde3f210>
contest = <cms.db.contest.Contest object at 0x7f30bc90f410>
timestamp = datetime.datetime(2026, 5, 13, 15, 18, 45, 240829)
username = 'otheruser', password = '', ip_address = IPv4Address('10.0.0.2')
admin_token = 'admin-token'

    def validate_login(
        sql_session: Session,
        contest: Contest,
        timestamp: datetime,
        username: str,
        password: str,
        ip_address: AnyIPAddress,
        admin_token: str = ""
    ) -> tuple[Participation | None, bytes | None]:
        """Authenticate a user logging in, with username and password.
    
        Given the information the user provided (the username and the
        password) and some context information (contest, to determine which
        users are allowed to log in, how and with which restrictions;
        timestamp for cookie creation; IP address to check against) try to
        authenticate the user and return its participation and the cookie
        to set to help authenticate future visits.
    
        After finding the participation, IP login and hidden users
        restrictions are checked.
    
        sql_session: the SQLAlchemy database session used to
            execute queries.
        contest: the contest the user is trying to access.
        timestamp: the date and the time of the request.
        username: the username the user provided.
        password: the password the user provided.
        ip_address: the IP address the request came from.
        admin_token: administrator's token used to impersonate a user
    
        return: if the user couldn't
            be authenticated then return None, otherwise return the
            participation that they wanted to authenticate as; if a cookie
            has to be set return it as well, otherwise return None.
    
        """
        def log_failed_attempt(msg, *args):
            logger.info("Unsuccessful login attempt from IP address %s, as user "
                        "%r, on contest %s, at %s: " + msg, ip_address,
                        username, contest.name, timestamp, *args)
    
        if not contest.allow_password_authentication and admin_token == "":
            log_failed_attempt("password authentication not allowed")
            return None, None
    
        participation: Participation | None = (
            sql_session.query(Participation)
            .join(Participation.user)
            .options(contains_eager(Participation.user))
            .filter(Participation.contest == contest)
            .filter(User.username == username)
            .first()
        )
    
        if participation is None:
            log_failed_attempt("user not registered to contest")
            return None, None
    
        if admin_token != "":
>           if config.contest_admin_token is None:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'Config' object has no attribute 'contest_admin_token'

.../server/contest/authentication.py:126: AttributeError
cmstestsuite/unit_tests/server/contest/authentication_test.py::TestAuthenticateRequest::test_ip_autologin
Stack Traces | 243s run time
self = <cmstestsuite.unit_tests.server.contest.authentication_test.TestAuthenticateRequest testMethod=test_ip_autologin>

    def setUp(self):
        super().setUp()
        self.timestamp = make_datetime()
        self.add_contest()
        self.contest = self.add_contest()
        self.user = self.add_user(
            username="myuser", password=build_password("mypass"))
        self.participation = self.add_participation(
            contest=self.contest, user=self.user)
        _, self.cookie = validate_login(
            self.session, self.contest, self.timestamp, self.user.username,
            "mypass", ipaddress.ip_address("10.0.0.1"))
    
        # For testing impersonation by admin token
        self.impersonated_user = self.add_user(username="otheruser")
        self.impersonated_participation = self.add_participation(
            contest=self.contest, user=self.impersonated_user)
        with patch.object(
            config.contest_web_server, "contest_admin_token", "admin-token"
        ):
>           _, self.impersonated_cookie = validate_login(
                self.session, self.contest, self.timestamp, "otheruser",
                "", ipaddress.ip_address("10.0.0.2"), "admin-token")

.../server/contest/authentication_test.py:209: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

sql_session = <sqlalchemy.orm.session.Session object at 0x7f30be5aa010>
contest = <cms.db.contest.Contest object at 0x7f30be26b110>
timestamp = datetime.datetime(2026, 5, 13, 15, 22, 27, 946592)
username = 'otheruser', password = '', ip_address = IPv4Address('10.0.0.2')
admin_token = 'admin-token'

    def validate_login(
        sql_session: Session,
        contest: Contest,
        timestamp: datetime,
        username: str,
        password: str,
        ip_address: AnyIPAddress,
        admin_token: str = ""
    ) -> tuple[Participation | None, bytes | None]:
        """Authenticate a user logging in, with username and password.
    
        Given the information the user provided (the username and the
        password) and some context information (contest, to determine which
        users are allowed to log in, how and with which restrictions;
        timestamp for cookie creation; IP address to check against) try to
        authenticate the user and return its participation and the cookie
        to set to help authenticate future visits.
    
        After finding the participation, IP login and hidden users
        restrictions are checked.
    
        sql_session: the SQLAlchemy database session used to
            execute queries.
        contest: the contest the user is trying to access.
        timestamp: the date and the time of the request.
        username: the username the user provided.
        password: the password the user provided.
        ip_address: the IP address the request came from.
        admin_token: administrator's token used to impersonate a user
    
        return: if the user couldn't
            be authenticated then return None, otherwise return the
            participation that they wanted to authenticate as; if a cookie
            has to be set return it as well, otherwise return None.
    
        """
        def log_failed_attempt(msg, *args):
            logger.info("Unsuccessful login attempt from IP address %s, as user "
                        "%r, on contest %s, at %s: " + msg, ip_address,
                        username, contest.name, timestamp, *args)
    
        if not contest.allow_password_authentication and admin_token == "":
            log_failed_attempt("password authentication not allowed")
            return None, None
    
        participation: Participation | None = (
            sql_session.query(Participation)
            .join(Participation.user)
            .options(contains_eager(Participation.user))
            .filter(Participation.contest == contest)
            .filter(User.username == username)
            .first()
        )
    
        if participation is None:
            log_failed_attempt("user not registered to contest")
            return None, None
    
        if admin_token != "":
>           if config.contest_admin_token is None:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'Config' object has no attribute 'contest_admin_token'

.../server/contest/authentication.py:126: AttributeError
cmstestsuite/unit_tests/server/contest/authentication_test.py::TestAuthenticateRequest::test_ip_autologin_with_ambiguous_addresses
Stack Traces | 243s run time
self = <cmstestsuite.unit_tests.server.contest.authentication_test.TestAuthenticateRequest testMethod=test_ip_autologin_with_ambiguous_addresses>

    def setUp(self):
        super().setUp()
        self.timestamp = make_datetime()
        self.add_contest()
        self.contest = self.add_contest()
        self.user = self.add_user(
            username="myuser", password=build_password("mypass"))
        self.participation = self.add_participation(
            contest=self.contest, user=self.user)
        _, self.cookie = validate_login(
            self.session, self.contest, self.timestamp, self.user.username,
            "mypass", ipaddress.ip_address("10.0.0.1"))
    
        # For testing impersonation by admin token
        self.impersonated_user = self.add_user(username="otheruser")
        self.impersonated_participation = self.add_participation(
            contest=self.contest, user=self.impersonated_user)
        with patch.object(
            config.contest_web_server, "contest_admin_token", "admin-token"
        ):
>           _, self.impersonated_cookie = validate_login(
                self.session, self.contest, self.timestamp, "otheruser",
                "", ipaddress.ip_address("10.0.0.2"), "admin-token")

.../server/contest/authentication_test.py:209: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

sql_session = <sqlalchemy.orm.session.Session object at 0x7f30be57c210>
contest = <cms.db.contest.Contest object at 0x7f30bc0999d0>
timestamp = datetime.datetime(2026, 5, 13, 15, 26, 30, 464206)
username = 'otheruser', password = '', ip_address = IPv4Address('10.0.0.2')
admin_token = 'admin-token'

    def validate_login(
        sql_session: Session,
        contest: Contest,
        timestamp: datetime,
        username: str,
        password: str,
        ip_address: AnyIPAddress,
        admin_token: str = ""
    ) -> tuple[Participation | None, bytes | None]:
        """Authenticate a user logging in, with username and password.
    
        Given the information the user provided (the username and the
        password) and some context information (contest, to determine which
        users are allowed to log in, how and with which restrictions;
        timestamp for cookie creation; IP address to check against) try to
        authenticate the user and return its participation and the cookie
        to set to help authenticate future visits.
    
        After finding the participation, IP login and hidden users
        restrictions are checked.
    
        sql_session: the SQLAlchemy database session used to
            execute queries.
        contest: the contest the user is trying to access.
        timestamp: the date and the time of the request.
        username: the username the user provided.
        password: the password the user provided.
        ip_address: the IP address the request came from.
        admin_token: administrator's token used to impersonate a user
    
        return: if the user couldn't
            be authenticated then return None, otherwise return the
            participation that they wanted to authenticate as; if a cookie
            has to be set return it as well, otherwise return None.
    
        """
        def log_failed_attempt(msg, *args):
            logger.info("Unsuccessful login attempt from IP address %s, as user "
                        "%r, on contest %s, at %s: " + msg, ip_address,
                        username, contest.name, timestamp, *args)
    
        if not contest.allow_password_authentication and admin_token == "":
            log_failed_attempt("password authentication not allowed")
            return None, None
    
        participation: Participation | None = (
            sql_session.query(Participation)
            .join(Participation.user)
            .options(contains_eager(Participation.user))
            .filter(Participation.contest == contest)
            .filter(User.username == username)
            .first()
        )
    
        if participation is None:
            log_failed_attempt("user not registered to contest")
            return None, None
    
        if admin_token != "":
>           if config.contest_admin_token is None:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'Config' object has no attribute 'contest_admin_token'

.../server/contest/authentication.py:126: AttributeError
cmstestsuite/unit_tests/server/contest/authentication_test.py::TestAuthenticateRequest::test_ip_lock
Stack Traces | 243s run time
self = <cmstestsuite.unit_tests.server.contest.authentication_test.TestAuthenticateRequest testMethod=test_ip_lock>

    def setUp(self):
        super().setUp()
        self.timestamp = make_datetime()
        self.add_contest()
        self.contest = self.add_contest()
        self.user = self.add_user(
            username="myuser", password=build_password("mypass"))
        self.participation = self.add_participation(
            contest=self.contest, user=self.user)
        _, self.cookie = validate_login(
            self.session, self.contest, self.timestamp, self.user.username,
            "mypass", ipaddress.ip_address("10.0.0.1"))
    
        # For testing impersonation by admin token
        self.impersonated_user = self.add_user(username="otheruser")
        self.impersonated_participation = self.add_participation(
            contest=self.contest, user=self.impersonated_user)
        with patch.object(
            config.contest_web_server, "contest_admin_token", "admin-token"
        ):
>           _, self.impersonated_cookie = validate_login(
                self.session, self.contest, self.timestamp, "otheruser",
                "", ipaddress.ip_address("10.0.0.2"), "admin-token")

.../server/contest/authentication_test.py:209: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

sql_session = <sqlalchemy.orm.session.Session object at 0x7f30be395050>
contest = <cms.db.contest.Contest object at 0x7f30b7fcf1d0>
timestamp = datetime.datetime(2026, 5, 13, 15, 30, 33, 73832)
username = 'otheruser', password = '', ip_address = IPv4Address('10.0.0.2')
admin_token = 'admin-token'

    def validate_login(
        sql_session: Session,
        contest: Contest,
        timestamp: datetime,
        username: str,
        password: str,
        ip_address: AnyIPAddress,
        admin_token: str = ""
    ) -> tuple[Participation | None, bytes | None]:
        """Authenticate a user logging in, with username and password.
    
        Given the information the user provided (the username and the
        password) and some context information (contest, to determine which
        users are allowed to log in, how and with which restrictions;
        timestamp for cookie creation; IP address to check against) try to
        authenticate the user and return its participation and the cookie
        to set to help authenticate future visits.
    
        After finding the participation, IP login and hidden users
        restrictions are checked.
    
        sql_session: the SQLAlchemy database session used to
            execute queries.
        contest: the contest the user is trying to access.
        timestamp: the date and the time of the request.
        username: the username the user provided.
        password: the password the user provided.
        ip_address: the IP address the request came from.
        admin_token: administrator's token used to impersonate a user
    
        return: if the user couldn't
            be authenticated then return None, otherwise return the
            participation that they wanted to authenticate as; if a cookie
            has to be set return it as well, otherwise return None.
    
        """
        def log_failed_attempt(msg, *args):
            logger.info("Unsuccessful login attempt from IP address %s, as user "
                        "%r, on contest %s, at %s: " + msg, ip_address,
                        username, contest.name, timestamp, *args)
    
        if not contest.allow_password_authentication and admin_token == "":
            log_failed_attempt("password authentication not allowed")
            return None, None
    
        participation: Participation | None = (
            sql_session.query(Participation)
            .join(Participation.user)
            .options(contains_eager(Participation.user))
            .filter(Participation.contest == contest)
            .filter(User.username == username)
            .first()
        )
    
        if participation is None:
            log_failed_attempt("user not registered to contest")
            return None, None
    
        if admin_token != "":
>           if config.contest_admin_token is None:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'Config' object has no attribute 'contest_admin_token'

.../server/contest/authentication.py:126: AttributeError
cmstestsuite/unit_tests/server/contest/authentication_test.py::TestAuthenticateRequest::test_no_participation_for_user_in_contest
Stack Traces | 262s run time
self = <cmstestsuite.unit_tests.server.contest.authentication_test.TestAuthenticateRequest testMethod=test_no_participation_for_user_in_contest>

    def setUp(self):
        super().setUp()
        self.timestamp = make_datetime()
        self.add_contest()
        self.contest = self.add_contest()
        self.user = self.add_user(
            username="myuser", password=build_password("mypass"))
        self.participation = self.add_participation(
            contest=self.contest, user=self.user)
        _, self.cookie = validate_login(
            self.session, self.contest, self.timestamp, self.user.username,
            "mypass", ipaddress.ip_address("10.0.0.1"))
    
        # For testing impersonation by admin token
        self.impersonated_user = self.add_user(username="otheruser")
        self.impersonated_participation = self.add_participation(
            contest=self.contest, user=self.impersonated_user)
        with patch.object(
            config.contest_web_server, "contest_admin_token", "admin-token"
        ):
>           _, self.impersonated_cookie = validate_login(
                self.session, self.contest, self.timestamp, "otheruser",
                "", ipaddress.ip_address("10.0.0.2"), "admin-token")

.../server/contest/authentication_test.py:209: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

sql_session = <sqlalchemy.orm.session.Session object at 0x7f30bc8be390>
contest = <cms.db.contest.Contest object at 0x7f30bc8bdb10>
timestamp = datetime.datetime(2026, 5, 13, 15, 34, 36, 87526)
username = 'otheruser', password = '', ip_address = IPv4Address('10.0.0.2')
admin_token = 'admin-token'

    def validate_login(
        sql_session: Session,
        contest: Contest,
        timestamp: datetime,
        username: str,
        password: str,
        ip_address: AnyIPAddress,
        admin_token: str = ""
    ) -> tuple[Participation | None, bytes | None]:
        """Authenticate a user logging in, with username and password.
    
        Given the information the user provided (the username and the
        password) and some context information (contest, to determine which
        users are allowed to log in, how and with which restrictions;
        timestamp for cookie creation; IP address to check against) try to
        authenticate the user and return its participation and the cookie
        to set to help authenticate future visits.
    
        After finding the participation, IP login and hidden users
        restrictions are checked.
    
        sql_session: the SQLAlchemy database session used to
            execute queries.
        contest: the contest the user is trying to access.
        timestamp: the date and the time of the request.
        username: the username the user provided.
        password: the password the user provided.
        ip_address: the IP address the request came from.
        admin_token: administrator's token used to impersonate a user
    
        return: if the user couldn't
            be authenticated then return None, otherwise return the
            participation that they wanted to authenticate as; if a cookie
            has to be set return it as well, otherwise return None.
    
        """
        def log_failed_attempt(msg, *args):
            logger.info("Unsuccessful login attempt from IP address %s, as user "
                        "%r, on contest %s, at %s: " + msg, ip_address,
                        username, contest.name, timestamp, *args)
    
        if not contest.allow_password_authentication and admin_token == "":
            log_failed_attempt("password authentication not allowed")
            return None, None
    
        participation: Participation | None = (
            sql_session.query(Participation)
            .join(Participation.user)
            .options(contains_eager(Participation.user))
            .filter(Participation.contest == contest)
            .filter(User.username == username)
            .first()
        )
    
        if participation is None:
            log_failed_attempt("user not registered to contest")
            return None, None
    
        if admin_token != "":
>           if config.contest_admin_token is None:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'Config' object has no attribute 'contest_admin_token'

.../server/contest/authentication.py:126: AttributeError
cmstestsuite/unit_tests/server/contest/authentication_test.py::TestAuthenticateRequest::test_no_user
Stack Traces | 533s run time
self = <cmstestsuite.unit_tests.server.contest.authentication_test.TestAuthenticateRequest testMethod=test_no_user>

    def setUp(self):
        super().setUp()
        self.timestamp = make_datetime()
        self.add_contest()
        self.contest = self.add_contest()
        self.user = self.add_user(
            username="myuser", password=build_password("mypass"))
        self.participation = self.add_participation(
            contest=self.contest, user=self.user)
        _, self.cookie = validate_login(
            self.session, self.contest, self.timestamp, self.user.username,
            "mypass", ipaddress.ip_address("10.0.0.1"))
    
        # For testing impersonation by admin token
        self.impersonated_user = self.add_user(username="otheruser")
        self.impersonated_participation = self.add_participation(
            contest=self.contest, user=self.impersonated_user)
        with patch.object(
            config.contest_web_server, "contest_admin_token", "admin-token"
        ):
>           _, self.impersonated_cookie = validate_login(
                self.session, self.contest, self.timestamp, "otheruser",
                "", ipaddress.ip_address("10.0.0.2"), "admin-token")

.../server/contest/authentication_test.py:209: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

sql_session = <sqlalchemy.orm.session.Session object at 0x7f30bdcf7f50>
contest = <cms.db.contest.Contest object at 0x7f30bdcf6fd0>
timestamp = datetime.datetime(2026, 5, 13, 15, 38, 58, 562614)
username = 'otheruser', password = '', ip_address = IPv4Address('10.0.0.2')
admin_token = 'admin-token'

    def validate_login(
        sql_session: Session,
        contest: Contest,
        timestamp: datetime,
        username: str,
        password: str,
        ip_address: AnyIPAddress,
        admin_token: str = ""
    ) -> tuple[Participation | None, bytes | None]:
        """Authenticate a user logging in, with username and password.
    
        Given the information the user provided (the username and the
        password) and some context information (contest, to determine which
        users are allowed to log in, how and with which restrictions;
        timestamp for cookie creation; IP address to check against) try to
        authenticate the user and return its participation and the cookie
        to set to help authenticate future visits.
    
        After finding the participation, IP login and hidden users
        restrictions are checked.
    
        sql_session: the SQLAlchemy database session used to
            execute queries.
        contest: the contest the user is trying to access.
        timestamp: the date and the time of the request.
        username: the username the user provided.
        password: the password the user provided.
        ip_address: the IP address the request came from.
        admin_token: administrator's token used to impersonate a user
    
        return: if the user couldn't
            be authenticated then return None, otherwise return the
            participation that they wanted to authenticate as; if a cookie
            has to be set return it as well, otherwise return None.
    
        """
        def log_failed_attempt(msg, *args):
            logger.info("Unsuccessful login attempt from IP address %s, as user "
                        "%r, on contest %s, at %s: " + msg, ip_address,
                        username, contest.name, timestamp, *args)
    
        if not contest.allow_password_authentication and admin_token == "":
            log_failed_attempt("password authentication not allowed")
            return None, None
    
        participation: Participation | None = (
            sql_session.query(Participation)
            .join(Participation.user)
            .options(contains_eager(Participation.user))
            .filter(Participation.contest == contest)
            .filter(User.username == username)
            .first()
        )
    
        if participation is None:
            log_failed_attempt("user not registered to contest")
            return None, None
    
        if admin_token != "":
>           if config.contest_admin_token is None:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'Config' object has no attribute 'contest_admin_token'

.../server/contest/authentication.py:126: AttributeError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@gollux gollux merged commit 15c547e into cms-dev:main May 13, 2026
4 checks passed
@gollux gollux deleted the api-auth-fix branch May 13, 2026 15:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants