Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env_sample
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
ENVIRONMENT="Development"
FLASK_SECRET_KEY="156a7fbb77c17708e41f044ea5131be2ad592a7deb21866bcb63d7801d2fa13e"
FLASK_DATABASE=compy.sqlite
# Password for the admin interface (http://localhost:5000/admin).
# For deployments, remove FLASK_ADMIN_PASSWORD and set FLASK_ADMIN_PASSWORD_HASH
# instead. Generate a hash with:
# python3 -c "from werkzeug.security import generate_password_hash; import getpass; print(generate_password_hash(getpass.getpass()))"
FLASK_ADMIN_PASSWORD="compy-admin"
#FLASK_ADMIN_PASSWORD_HASH=""
13 changes: 9 additions & 4 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,12 @@ Install Weasyprint https://doc.courtbouillon.org/weasyprint/stable/first_steps.h
- Execute `git clone https://github.com/Azrael3000/Compy.git`
- Switch to the new folder: `cd Compy`
- Set up the environmen: `cp .env_sample .env`
- For deployments you MUST edit the .env file and provide a new secret. A new one can be generated e.g. by running
`python3 -c "import secrets; print(secrets.token_hex())"`
- For deployments you MUST edit the .env file:
- Provide a new secret (used to sign the admin session cookie). A new one can be generated e.g. by running
`python3 -c "import secrets; print(secrets.token_hex())"`
- Set your own admin password. Either change `FLASK_ADMIN_PASSWORD`, or (recommended) remove it and set
`FLASK_ADMIN_PASSWORD_HASH` to a password hash generated by running
`python3 -c "from werkzeug.security import generate_password_hash; import getpass; print(generate_password_hash(getpass.getpass()))"`
- Start a virtual environment and install required packages:
- Linux: `source venv/bin/activate && pip install -r requirements.txt`
- Set up the database and run the server: `python3 compy.py --init_db`
Expand All @@ -73,8 +77,9 @@ Install Weasyprint https://doc.courtbouillon.org/weasyprint/stable/first_steps.h
- Linux: `python3 compy.py`
- Windows: `python3.exe compy.py`
- Navigate your browser to `localhost:5000`
- The admin interface is at `localhost:5000/admin?auth=XXXXXX` where `XXXXXX` are the first 6
characters of your `FLASK_SECRET_KEY` from `.env`
- The admin interface is at `localhost:5000/admin`. It asks for the admin password configured in
`.env` (`FLASK_ADMIN_PASSWORD` or `FLASK_ADMIN_PASSWORD_HASH`); a login is valid for 12 hours
or until you press "Logout"

## Test data

Expand Down
72 changes: 61 additions & 11 deletions compy_concurrency_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class TestConcurrentPages(compy_testing.CompyServerTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
session = requests.Session()
session = cls.adminSession()

# name the default competition and upload the excel file
response = session.post(cls.base_url + "/competition",
Expand Down Expand Up @@ -119,18 +119,20 @@ def registrationWrites(self, session, round_index):

def testConcurrentPagesDoNotInterfere(self):
page_simulations = [
("admin1", self.adminTabCompOne),
("admin2", self.adminTabCompTwo),
("clock", self.clockDisplay),
("judge", self.judgePhone),
("results", self.publicResultsPage),
("registration", self.registrationWrites),
("admin1", self.adminTabCompOne, True),
("admin2", self.adminTabCompTwo, True),
("clock", self.clockDisplay, False),
("judge", self.judgePhone, False),
("results", self.publicResultsPage, False),
("registration", self.registrationWrites, True),
]
failures = []
stop_event = threading.Event()

def run_page(page_name, request_round):
page_session = requests.Session()
def run_page(page_name, request_round, is_admin_page):
# admin pages carry a session cookie, public pages must work
# without any authentication
page_session = self.adminSession() if is_admin_page else requests.Session()
for round_index in range(N_ROUNDS):
if stop_event.is_set():
return
Expand All @@ -151,7 +153,7 @@ def run_page(page_name, request_round):
self.assertEqual(failures, [])

# after the storm: comp 1 must be fully intact
session = requests.Session()
session = self.adminSession()
response = session.post(self.base_url + "/load_comp", json={"comp_id": self.comp_one_id})
self.assertEqual(response.json()["comp_name"], "Comp One")
self.assertEqual(len(response.json()["athletes"]), 30)
Expand All @@ -175,9 +177,57 @@ def testForgedJudgeHashIsRejected(self):
"block": self.first_block, "lane": "1"})
self.assertEqual(response.status_code, 404)
# ...and it must not have switched or broken anything
response = self.adminSession().get(self.base_url + "/athletes",
params={"comp_id": self.comp_one_id})
self.assertEqual(len(response.json()["athletes"]), 30)

def testAdminEndpointsRequireLogin(self):
# without a session cookie all admin endpoints must refuse to act
response = requests.get(self.base_url + "/athletes",
params={"comp_id": self.comp_one_id})
self.assertEqual(len(response.json()["athletes"]), 30)
self.assertEqual(response.status_code, 401)
response = requests.post(self.base_url + "/competition",
json={"comp_name": "Hacked", "overwrite": True,
"comp_id": self.comp_one_id})
self.assertEqual(response.status_code, 401)
response = requests.delete(self.base_url + "/competition",
json={"comp_id": self.comp_one_id})
self.assertEqual(response.status_code, 401)
# the admin page itself redirects to the login form
response = requests.get(self.base_url + "/admin", allow_redirects=False)
self.assertEqual(response.status_code, 302)
self.assertTrue(response.headers["Location"].endswith("/admin/login"))
# ...and nothing was changed by the rejected requests
response = self.adminSession().post(self.base_url + "/load_comp",
json={"comp_id": self.comp_one_id})
self.assertEqual(response.json()["comp_name"], "Comp One")

def testWrongPasswordIsRejected(self):
session = requests.Session()
response = session.post(self.base_url + "/admin/login",
data={"password": "not-the-password"})
self.assertEqual(response.status_code, 401)
response = session.get(self.base_url + "/athletes",
params={"comp_id": self.comp_one_id})
self.assertEqual(response.status_code, 401)

def testJudgeCanSaveResultWithoutAdminSession(self):
# a judge phone is not logged in as admin; the judge hash from the
# QR code must be enough to save a result, a forged hash must not be
response = requests.get(self.base_url + "/judge/athletes",
params={"comp_id": self.comp_one_id, "judge_id": self.judge_id,
"judge_hash": self.judge_hash, "day": self.first_day,
"block": self.first_block, "lane": "1"})
start_id = response.json()["lane_list"][0]["s_id"]
result = {"comp_id": self.comp_one_id, "judge_id": self.judge_id,
"id": start_id, "rp": "", "penalty": 0, "card": "WHITE",
"remarks": "", "judge_remarks": ""}
response = requests.put(self.base_url + "/result",
json=result | {"judge_hash": "deadbeef"})
self.assertEqual(response.status_code, 401)
response = requests.put(self.base_url + "/result",
json=result | {"judge_hash": self.judge_hash})
self.assertEqual(response.status_code, 200)


if __name__ == '__main__':
Expand Down
Loading