diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index fbc6a94..b0e07db 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -13,6 +13,9 @@ jobs: env: # Database for tests DATABASE_URL: postgres://postgres:postgres@localhost/mobilityprofile + TOKEN_SECRET: dgj533GDG242fdsM + + steps: diff --git a/account/tests/test_api.py b/account/tests/test_api.py index 028b5cf..b063537 100644 --- a/account/tests/test_api.py +++ b/account/tests/test_api.py @@ -32,7 +32,6 @@ def test_token_expiration(api_client_authenticated, users, profiles): @pytest.mark.django_db def test_unauthenticated_cannot_do_anything(api_client, users): - # TODO, add start-poll url after recaptcha integration urls = [ reverse("account:profiles-detail", args=[users.get(username="test1").id]), ] diff --git a/config_dev.env.example b/config_dev.env.example index e3851c7..1a48887 100644 --- a/config_dev.env.example +++ b/config_dev.env.example @@ -53,3 +53,6 @@ STATIC_URL=/static/ # Location of memcached CACHE_LOCATION=127.0.0.1:11211 + +# Must be 16 char long +TOKEN_SECRET= \ No newline at end of file diff --git a/mpbackend/settings.py b/mpbackend/settings.py index bfa5e8e..84d8e8e 100644 --- a/mpbackend/settings.py +++ b/mpbackend/settings.py @@ -23,6 +23,7 @@ STATIC_URL=(str, "/static/"), CORS_ORIGIN_WHITELIST=(list, []), CACHE_LOCATION=(str, "127.0.0.1:11211"), + TOKEN_SECRET=(str, None), ) # WARN about env file not being preset. Here we pre-empt it. env_file_path = os.path.join(BASE_DIR, CONFIG_FILE_NAME) @@ -98,6 +99,7 @@ CSRF_TRUSTED_ORIGINS = ["http://localhost:8080"] SECURE_CROSS_ORIGIN_OPENER_POLICY = None +TOKEN_SECRET = env("TOKEN_SECRET") REST_FRAMEWORK = { "DEFAULT_PERMISSION_CLASSES": [ diff --git a/profiles/api/views.py b/profiles/api/views.py index fc83597..bd35944 100644 --- a/profiles/api/views.py +++ b/profiles/api/views.py @@ -54,7 +54,7 @@ SubQuestion, SubQuestionCondition, ) -from profiles.utils import generate_password, get_user_result +from profiles.utils import encrypt_text, generate_password, get_user_result from .utils import PostalCodeResultFilter @@ -187,7 +187,6 @@ def list(self, request, *args, **kwargs): permission_classes=[AllowAny], ) def start_poll(self, request): - # TODO check recaptha uuid4 = uuid.uuid4() username = f"anonymous_{str(uuid4)}" user = User.objects.create(pk=uuid4, username=username, is_generated=True) @@ -196,7 +195,8 @@ def start_poll(self, request): user.profile = Profile.objects.create(user=user) user.save() token, _ = Token.objects.get_or_create(user=user) - response_data = {"token": token.key, "id": user.id} + data = encrypt_text(token.key, settings.TOKEN_SECRET) + response_data = {"data": data, "id": user.id} return Response(response_data, status=status.HTTP_200_OK) @extend_schema( diff --git a/profiles/tests/api/test_postal_code_result.py b/profiles/tests/api/test_postal_code_result.py index 358b378..572ee3c 100644 --- a/profiles/tests/api/test_postal_code_result.py +++ b/profiles/tests/api/test_postal_code_result.py @@ -147,7 +147,6 @@ def test_postal_code_result( ): num_users = 5 num_answers = 0 - start_poll_url = reverse("profiles:question-start-poll") postal_codes = [None, "20100", "20200", "20100", None] q1 = questions_test_result.get(number="1") q1_option_pos = options_test_result.get(question=q1, value=POS) @@ -155,22 +154,19 @@ def test_postal_code_result( # post positive for i in range(num_users): - response = api_client.post(start_poll_url) - token = response.json()["token"] - assert response.status_code == 200 - token = response.json()["token"] - user_id = response.json()["id"] + user = User.objects.create(username=f"user_{i}") + Profile.objects.create(user=user) + token = Token.objects.create(user=user) + api_client.credentials(HTTP_AUTHORIZATION="Token " + token.key) assert User.objects.all().count() == 1 + i - user = User.objects.get(id=user_id) user.profile.postal_code = postal_codes[i] user.profile.optional_postal_code = postal_codes[i] user.profile.save() - api_client.credentials(HTTP_AUTHORIZATION=f"Token {token}") api_client.post(ANSWER_URL, {"option": q1_option_pos.id, "question": q1.id}) num_answers += 1 assert Answer.objects.count() == num_answers - response = api_client.post(reverse("profiles:question-end-poll")) - api_client.credentials() + api_client.post(reverse("profiles:question-end-poll")) + assert PostalCodeResult.objects.count() == 6 assert PostalCode.objects.count() == 3 assert PostalCodeType.objects.count() == 2 @@ -213,13 +209,12 @@ def test_postal_code_result( # post negative, but only to user Home postal code for i in range(num_users): - response = api_client.post(start_poll_url) - token = response.json()["token"] - assert response.status_code == 200 - token = response.json()["token"] - user_id = response.json()["id"] + user = User.objects.create(username=f"neg_user_{i}") + Profile.objects.create(user=user) + token = Token.objects.create(user=user) + api_client.credentials(HTTP_AUTHORIZATION="Token " + token.key) + assert User.objects.all().count() == num_users + 1 + i - user = User.objects.get(id=user_id) user.profile.postal_code = postal_codes[i] user.profile.optional_postal_code = None user.profile.save() @@ -227,8 +222,8 @@ def test_postal_code_result( api_client.post(ANSWER_URL, {"option": q1_option_neg.id, "question": q1.id}) num_answers += 1 assert Answer.objects.count() == num_answers - response = api_client.post(reverse("profiles:question-end-poll")) - api_client.credentials() + api_client.post(reverse("profiles:question-end-poll")) + neg_result = results_test_result.get(topic=NEG) pos_result = results_test_result.get(topic=POS) diff --git a/profiles/tests/test_crypt.py b/profiles/tests/test_crypt.py new file mode 100644 index 0000000..f397409 --- /dev/null +++ b/profiles/tests/test_crypt.py @@ -0,0 +1,9 @@ +from profiles.utils import decrypt_text, encrypt_text + + +def test_encrypt_and_decrypt(): + in_text = "Hello World" + key = "1234567890123456" + ret_vals = encrypt_text(in_text, key) + dec_text = decrypt_text(ret_vals[0], key, ret_vals[1]) + assert dec_text == in_text diff --git a/profiles/utils.py b/profiles/utils.py index 67d227e..d599c2b 100644 --- a/profiles/utils.py +++ b/profiles/utils.py @@ -1,10 +1,31 @@ +import base64 +import os import secrets import string +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad, unpad + from account.models import User from profiles.models import Answer, Result +def encrypt_text(text, key): + text = pad(text.encode(), 16) + iv = os.urandom(16) + cipher = AES.new(key.encode("utf-8"), AES.MODE_CBC, iv) + return base64.b64encode(cipher.encrypt(text)), base64.b64encode(cipher.iv).decode( + "utf-8" + ) + + +def decrypt_text(enc, key, iv): + iv = base64.b64decode(iv) + enc = base64.b64decode(enc) + cipher = AES.new(key.encode("utf-8"), AES.MODE_CBC, iv) + return unpad(cipher.decrypt(enc), 16).decode() + + def get_user_result(user: User) -> Result: answer_qs = Answer.objects.filter(user=user) if answer_qs.count() == 0: diff --git a/requirements.in b/requirements.in index 146d860..ac8ac3f 100644 --- a/requirements.in +++ b/requirements.in @@ -17,3 +17,4 @@ django-cors-headers freezegun django-filter pymemcache +pycryptodome diff --git a/requirements.txt b/requirements.txt index abe665e..0f91efd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,9 +17,7 @@ click==8.1.3 # black # pip-tools coverage[toml]==7.2.3 - # via - # coverage - # pytest-cov + # via pytest-cov django==4.2.10 # via # -r requirements.in @@ -88,6 +86,8 @@ psycopg2-binary==2.9.6 # via -r requirements.in pycodestyle==2.10.0 # via flake8 +pycryptodome==3.20.0 + # via -r requirements.in pyflakes==3.0.1 # via flake8 pymemcache==4.0.0 @@ -123,7 +123,6 @@ tomli==2.0.1 # black # build # coverage - # pyproject-hooks # pytest typing-extensions==4.5.0 # via django-modeltranslation