From ab70687c6196f021e6800e8ea424517c1b9a1130 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 19 Dec 2022 12:57:32 +0000 Subject: [PATCH 01/34] fix(tokens-repository) :make NONE-uses-left eternally valid and test for it --- .../tokens/json_tokens_repository.py | 3 +- .../tokens/redis_tokens_repository.py | 5 +- .../test_repository/test_tokens_repository.py | 74 +++++++------------ 3 files changed, 32 insertions(+), 50 deletions(-) diff --git a/selfprivacy_api/repositories/tokens/json_tokens_repository.py b/selfprivacy_api/repositories/tokens/json_tokens_repository.py index b4c0ab2..c599e0f 100644 --- a/selfprivacy_api/repositories/tokens/json_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/json_tokens_repository.py @@ -98,7 +98,8 @@ class JsonTokensRepository(AbstractTokensRepository): """Decrement recovery key use count by one""" if self.is_recovery_key_valid(): with WriteUserData(UserDataFiles.TOKENS) as tokens: - tokens["recovery_token"]["uses_left"] -= 1 + if tokens["recovery_token"]["uses_left"] is not None: + tokens["recovery_token"]["uses_left"] -= 1 def get_new_device_key(self) -> NewDeviceKey: """Creates and returns the new device key""" diff --git a/selfprivacy_api/repositories/tokens/redis_tokens_repository.py b/selfprivacy_api/repositories/tokens/redis_tokens_repository.py index b1fb4b0..2fb6180 100644 --- a/selfprivacy_api/repositories/tokens/redis_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/redis_tokens_repository.py @@ -92,8 +92,9 @@ class RedisTokensRepository(AbstractTokensRepository): """Decrement recovery key use count by one""" if self.is_recovery_key_valid(): uses_left = self.get_recovery_key().uses_left - r = self.connection - r.hset(RECOVERY_KEY_REDIS_KEY, "uses_left", uses_left - 1) + if uses_left is not None: + r = self.connection + r.hset(RECOVERY_KEY_REDIS_KEY, "uses_left", uses_left - 1) def _get_stored_new_device_key(self) -> Optional[NewDeviceKey]: """Retrieves new device key that is already stored.""" diff --git a/tests/test_graphql/test_repository/test_tokens_repository.py b/tests/test_graphql/test_repository/test_tokens_repository.py index 43f7626..7a80b03 100644 --- a/tests/test_graphql/test_repository/test_tokens_repository.py +++ b/tests/test_graphql/test_repository/test_tokens_repository.py @@ -3,6 +3,7 @@ # pylint: disable=missing-function-docstring from datetime import datetime +from mnemonic import Mnemonic import pytest @@ -132,21 +133,6 @@ def mock_recovery_key_generate(mocker): return mock -@pytest.fixture -def mock_recovery_key_generate_for_mnemonic(mocker): - mock = mocker.patch( - "selfprivacy_api.models.tokens.recovery_key.RecoveryKey.generate", - autospec=True, - return_value=RecoveryKey( - key="ed653e4b8b042b841d285fa7a682fa09e925ddb2d8906f54", - created_at=datetime(2022, 7, 15, 17, 41, 31, 675698), - expires_at=None, - uses_left=1, - ), - ) - return mock - - @pytest.fixture def empty_json_repo(empty_keys): repo = JsonTokensRepository() @@ -397,46 +383,40 @@ def test_use_not_found_mnemonic_recovery_key(some_tokens_repo): ) -def test_use_mnemonic_recovery_key_when_empty(empty_repo): - repo = empty_repo - - with pytest.raises(RecoveryKeyNotFound): - assert ( - repo.use_mnemonic_recovery_key( - mnemonic_phrase="captain ribbon toddler settle symbol minute step broccoli bless universe divide bulb", - device_name="primary_token", - ) - is None - ) +@pytest.fixture(params=["recovery_uses_1", "recovery_eternal"]) +def recovery_key_uses_left(request): + if request.param == "recovery_uses_1": + return 1 + if request.param == "recovery_eternal": + return None -# agnostic test mixed with an implementation test -def test_use_mnemonic_recovery_key( - some_tokens_repo, mock_recovery_key_generate_for_mnemonic, mock_generate_token -): +def test_use_mnemonic_recovery_key(some_tokens_repo, recovery_key_uses_left): repo = some_tokens_repo - assert repo.create_recovery_key(uses_left=1, expiration=None) is not None - - test_token = Token( - token="ur71mC4aiI6FIYAN--cTL-38rPHS5D6NuB1bgN_qKF4", - device_name="newdevice", - created_at=datetime(2022, 11, 14, 6, 6, 32, 777123), - ) - assert ( - repo.use_mnemonic_recovery_key( - mnemonic_phrase="uniform clarify napkin bid dress search input armor police cross salon because myself uphold slice bamboo hungry park", - device_name="newdevice", - ) - == test_token + repo.create_recovery_key(uses_left=recovery_key_uses_left, expiration=None) + is not None + ) + assert repo.is_recovery_key_valid() + recovery_key = repo.get_recovery_key() + + token = repo.use_mnemonic_recovery_key( + mnemonic_phrase=Mnemonic(language="english").to_mnemonic( + bytes.fromhex(recovery_key.key) + ), + device_name="newdevice", ) - assert test_token in repo.get_tokens() + assert token.device_name == "newdevice" + assert token in repo.get_tokens() + new_uses = None + if recovery_key_uses_left is not None: + new_uses = recovery_key_uses_left - 1 assert repo.get_recovery_key() == RecoveryKey( - key="ed653e4b8b042b841d285fa7a682fa09e925ddb2d8906f54", - created_at=datetime(2022, 7, 15, 17, 41, 31, 675698), + key=recovery_key.key, + created_at=recovery_key.created_at, expires_at=None, - uses_left=0, + uses_left=new_uses, ) From a97705ef256f9c25a11e7e571664dbc9b8def7cf Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 19 Dec 2022 17:37:44 +0000 Subject: [PATCH 02/34] fix(tokens-repository): fix getting and setting recovery token expiration date --- .../tokens/json_tokens_repository.py | 7 +++++-- .../test_repository/test_tokens_repository.py | 21 ++++++++++++++++++- tests/test_models.py | 10 +++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 tests/test_models.py diff --git a/selfprivacy_api/repositories/tokens/json_tokens_repository.py b/selfprivacy_api/repositories/tokens/json_tokens_repository.py index c599e0f..e43a3eb 100644 --- a/selfprivacy_api/repositories/tokens/json_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/json_tokens_repository.py @@ -69,7 +69,7 @@ class JsonTokensRepository(AbstractTokensRepository): recovery_key = RecoveryKey( key=tokens_file["recovery_token"].get("token"), created_at=tokens_file["recovery_token"].get("date"), - expires_at=tokens_file["recovery_token"].get("expitation"), + expires_at=tokens_file["recovery_token"].get("expiration"), uses_left=tokens_file["recovery_token"].get("uses_left"), ) @@ -85,10 +85,13 @@ class JsonTokensRepository(AbstractTokensRepository): recovery_key = RecoveryKey.generate(expiration, uses_left) with WriteUserData(UserDataFiles.TOKENS) as tokens_file: + expiration = recovery_key.expires_at + if expiration is not None: + expiration = expiration.strftime(DATETIME_FORMAT) tokens_file["recovery_token"] = { "token": recovery_key.key, "date": recovery_key.created_at.strftime(DATETIME_FORMAT), - "expiration": recovery_key.expires_at, + "expiration": expiration, "uses_left": recovery_key.uses_left, } diff --git a/tests/test_graphql/test_repository/test_tokens_repository.py b/tests/test_graphql/test_repository/test_tokens_repository.py index 7a80b03..85cee00 100644 --- a/tests/test_graphql/test_repository/test_tokens_repository.py +++ b/tests/test_graphql/test_repository/test_tokens_repository.py @@ -2,7 +2,7 @@ # pylint: disable=unused-argument # pylint: disable=missing-function-docstring -from datetime import datetime +from datetime import datetime, timedelta from mnemonic import Mnemonic import pytest @@ -341,6 +341,25 @@ def test_use_mnemonic_not_valid_recovery_key( ) +def test_use_mnemonic_expired_recovery_key( + some_tokens_repo, +): + repo = some_tokens_repo + expiration = datetime.now() - timedelta(minutes=5) + assert repo.create_recovery_key(uses_left=2, expiration=expiration) is not None + recovery_key = repo.get_recovery_key() + assert recovery_key.expires_at == expiration + assert not repo.is_recovery_key_valid() + + with pytest.raises(RecoveryKeyNotFound): + token = repo.use_mnemonic_recovery_key( + mnemonic_phrase=Mnemonic(language="english").to_mnemonic( + bytes.fromhex(recovery_key.key) + ), + device_name="newdevice", + ) + + def test_use_mnemonic_not_mnemonic_recovery_key(some_tokens_repo): repo = some_tokens_repo assert repo.create_recovery_key(uses_left=1, expiration=None) is not None diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..078dfd5 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,10 @@ +import pytest +from datetime import datetime, timedelta + +from selfprivacy_api.models.tokens.recovery_key import RecoveryKey + + +def test_recovery_key_expired(): + expiration = datetime.now() - timedelta(minutes=5) + key = RecoveryKey.generate(expiration=expiration, uses_left=2) + assert not key.is_valid() From 009a89fa020f5eb471a53ca6fb183e9b1469cb88 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 21 Dec 2022 10:24:32 +0000 Subject: [PATCH 03/34] refactor(tokens-repo): use token repo for graphql use_recovery_api_key --- .../graphql/mutations/api_mutations.py | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/selfprivacy_api/graphql/mutations/api_mutations.py b/selfprivacy_api/graphql/mutations/api_mutations.py index c6727db..1a67212 100644 --- a/selfprivacy_api/graphql/mutations/api_mutations.py +++ b/selfprivacy_api/graphql/mutations/api_mutations.py @@ -22,10 +22,19 @@ from selfprivacy_api.utils.auth import ( delete_new_device_auth_token, get_new_device_auth_token, refresh_token, - use_mnemonic_recoverery_token, use_new_device_auth_token, ) +from selfprivacy_api.repositories.tokens.json_tokens_repository import ( + JsonTokensRepository, +) +from selfprivacy_api.repositories.tokens.exceptions import ( + RecoveryKeyNotFound, + InvalidMnemonic, +) + +TOKEN_REPO = JsonTokensRepository() + @strawberry.type class ApiKeyMutationReturn(MutationReturnInterface): @@ -98,20 +107,21 @@ class ApiMutations: self, input: UseRecoveryKeyInput ) -> DeviceApiTokenMutationReturn: """Use recovery key""" - token = use_mnemonic_recoverery_token(input.key, input.deviceName) - if token is None: + try: + token = TOKEN_REPO.use_mnemonic_recovery_key(input.key, input.deviceName) + return DeviceApiTokenMutationReturn( + success=True, + message="Recovery key used", + code=200, + token=token.token, + ) + except (RecoveryKeyNotFound, InvalidMnemonic): return DeviceApiTokenMutationReturn( success=False, message="Recovery key not found", code=404, token=None, ) - return DeviceApiTokenMutationReturn( - success=True, - message="Recovery key used", - code=200, - token=token, - ) @strawberry.mutation(permission_classes=[IsAuthenticated]) def refresh_device_api_token(self, info: Info) -> DeviceApiTokenMutationReturn: From d3bf867bb54f9afd23b07fabcdac7c0c43b429ee Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 21 Dec 2022 13:05:00 +0000 Subject: [PATCH 04/34] fix(tokens-repo): do not change the date on token refresh --- .../tokens/abstract_tokens_repository.py | 1 + .../test_repository/test_tokens_repository.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py index a67d62d..4f86e61 100644 --- a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py @@ -52,6 +52,7 @@ class AbstractTokensRepository(ABC): def refresh_token(self, input_token: Token) -> Token: """Change the token field of the existing token""" new_token = Token.generate(device_name=input_token.device_name) + new_token.created_at = input_token.created_at if input_token in self.get_tokens(): self.delete_token(input_token) diff --git a/tests/test_graphql/test_repository/test_tokens_repository.py b/tests/test_graphql/test_repository/test_tokens_repository.py index 85cee00..1a26247 100644 --- a/tests/test_graphql/test_repository/test_tokens_repository.py +++ b/tests/test_graphql/test_repository/test_tokens_repository.py @@ -266,15 +266,17 @@ def test_delete_not_found_token(some_tokens_repo): assert token in new_tokens -def test_refresh_token(some_tokens_repo, mock_token_generate): +def test_refresh_token(some_tokens_repo): repo = some_tokens_repo input_token = some_tokens_repo.get_tokens()[0] - assert repo.refresh_token(input_token) == Token( - token="ZuLNKtnxDeq6w2dpOJhbB3iat_sJLPTPl_rN5uc5MvM", - device_name="IamNewDevice", - created_at=datetime(2022, 7, 15, 17, 41, 31, 675698), - ) + output_token = repo.refresh_token(input_token) + + assert output_token.token != input_token.token + assert output_token.device_name == input_token.device_name + assert output_token.created_at == input_token.created_at + + assert output_token in repo.get_tokens() def test_refresh_not_found_token(some_tokens_repo, mock_token_generate): From 39277419acbe61b70757cb7999148f0acb4f26f3 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 21 Dec 2022 13:09:51 +0000 Subject: [PATCH 05/34] refactor(tokens-repo): use tokens-repo to refresh tokens --- .../graphql/mutations/api_mutations.py | 24 ++++++++++--------- tests/test_graphql/test_api_devices.py | 24 ++++++++++++++----- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/selfprivacy_api/graphql/mutations/api_mutations.py b/selfprivacy_api/graphql/mutations/api_mutations.py index 1a67212..3626d83 100644 --- a/selfprivacy_api/graphql/mutations/api_mutations.py +++ b/selfprivacy_api/graphql/mutations/api_mutations.py @@ -21,7 +21,6 @@ from selfprivacy_api.graphql.mutations.mutation_interface import ( from selfprivacy_api.utils.auth import ( delete_new_device_auth_token, get_new_device_auth_token, - refresh_token, use_new_device_auth_token, ) @@ -126,32 +125,35 @@ class ApiMutations: @strawberry.mutation(permission_classes=[IsAuthenticated]) def refresh_device_api_token(self, info: Info) -> DeviceApiTokenMutationReturn: """Refresh device api token""" - token = ( + token_string = ( info.context["request"] .headers.get("Authorization", "") .replace("Bearer ", "") ) - if token is None: + if token_string is None: return DeviceApiTokenMutationReturn( success=False, message="Token not found", code=404, token=None, ) - new_token = refresh_token(token) - if new_token is None: + + try: + old_token = TOKEN_REPO.get_token_by_token_string(token_string) + new_token = TOKEN_REPO.refresh_token(old_token) + return DeviceApiTokenMutationReturn( + success=True, + message="Token refreshed", + code=200, + token=new_token.token, + ) + except: return DeviceApiTokenMutationReturn( success=False, message="Token not found", code=404, token=None, ) - return DeviceApiTokenMutationReturn( - success=True, - message="Token refreshed", - code=200, - token=new_token, - ) @strawberry.mutation(permission_classes=[IsAuthenticated]) def delete_device_api_token(self, device: str, info: Info) -> GenericMutationReturn: diff --git a/tests/test_graphql/test_api_devices.py b/tests/test_graphql/test_api_devices.py index d8dc974..07cf42a 100644 --- a/tests/test_graphql/test_api_devices.py +++ b/tests/test_graphql/test_api_devices.py @@ -2,8 +2,14 @@ # pylint: disable=unused-argument # pylint: disable=missing-function-docstring import datetime +import pytest from mnemonic import Mnemonic +from selfprivacy_api.repositories.tokens.json_tokens_repository import ( + JsonTokensRepository, +) +from selfprivacy_api.models.tokens.token import Token + from tests.common import generate_api_query, read_json, write_json TOKENS_FILE_CONTETS = { @@ -30,6 +36,11 @@ devices { """ +@pytest.fixture +def token_repo(): + return JsonTokensRepository() + + def test_graphql_tokens_info(authorized_client, tokens_file): response = authorized_client.post( "/graphql", @@ -170,7 +181,7 @@ def test_graphql_refresh_token_unauthorized(client, tokens_file): assert response.json()["data"] is None -def test_graphql_refresh_token(authorized_client, tokens_file): +def test_graphql_refresh_token(authorized_client, tokens_file, token_repo): response = authorized_client.post( "/graphql", json={"query": REFRESH_TOKEN_MUTATION}, @@ -180,11 +191,12 @@ def test_graphql_refresh_token(authorized_client, tokens_file): assert response.json()["data"]["refreshDeviceApiToken"]["success"] is True assert response.json()["data"]["refreshDeviceApiToken"]["message"] is not None assert response.json()["data"]["refreshDeviceApiToken"]["code"] == 200 - assert read_json(tokens_file)["tokens"][0] == { - "token": response.json()["data"]["refreshDeviceApiToken"]["token"], - "name": "test_token", - "date": "2022-01-14 08:31:10.789314", - } + token = token_repo.get_token_by_name("test_token") + assert token == Token( + token=response.json()["data"]["refreshDeviceApiToken"]["token"], + device_name="test_token", + created_at=datetime.datetime(2022, 1, 14, 8, 31, 10, 789314), + ) NEW_DEVICE_KEY_MUTATION = """ From 16f71b0b09933b80420346df3f72de7fa6662aab Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 21 Dec 2022 13:51:39 +0000 Subject: [PATCH 06/34] refactor(tokens-repo): use tokens-repo to create recovery token --- selfprivacy_api/actions/api_tokens.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/selfprivacy_api/actions/api_tokens.py b/selfprivacy_api/actions/api_tokens.py index 61c695d..33ab286 100644 --- a/selfprivacy_api/actions/api_tokens.py +++ b/selfprivacy_api/actions/api_tokens.py @@ -2,11 +2,11 @@ from datetime import datetime from typing import Optional from pydantic import BaseModel +from mnemonic import Mnemonic from selfprivacy_api.utils.auth import ( delete_token, - generate_recovery_token, get_recovery_token_status, get_tokens_info, is_recovery_token_exists, @@ -17,6 +17,12 @@ from selfprivacy_api.utils.auth import ( get_token_name, ) +from selfprivacy_api.repositories.tokens.json_tokens_repository import ( + JsonTokensRepository, +) + +TOKEN_REPO = JsonTokensRepository() + class TokenInfoWithIsCaller(BaseModel): """Token info""" @@ -112,5 +118,6 @@ def get_new_api_recovery_key( if uses_left <= 0: raise InvalidUsesLeft("Uses must be greater than 0") - key = generate_recovery_token(expiration_date, uses_left) - return key + key = TOKEN_REPO.create_recovery_key(expiration_date, uses_left) + mnemonic_phrase = Mnemonic(language="english").to_mnemonic(bytes.fromhex(key.key)) + return mnemonic_phrase From 3021584adc57aa5a0abff28009339cde4b940304 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 21 Dec 2022 15:23:42 +0000 Subject: [PATCH 07/34] refactor(tokens-repo): delete refresh_token() from auth --- selfprivacy_api/actions/api_tokens.py | 10 ++++++---- selfprivacy_api/graphql/mutations/api_mutations.py | 8 ++++---- selfprivacy_api/utils/auth.py | 11 ----------- tests/test_rest_endpoints/test_auth.py | 8 +++++++- 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/selfprivacy_api/actions/api_tokens.py b/selfprivacy_api/actions/api_tokens.py index 33ab286..38cd208 100644 --- a/selfprivacy_api/actions/api_tokens.py +++ b/selfprivacy_api/actions/api_tokens.py @@ -13,13 +13,13 @@ from selfprivacy_api.utils.auth import ( is_recovery_token_valid, is_token_name_exists, is_token_name_pair_valid, - refresh_token, get_token_name, ) from selfprivacy_api.repositories.tokens.json_tokens_repository import ( JsonTokensRepository, ) +from selfprivacy_api.repositories.tokens.exceptions import TokenNotFound TOKEN_REPO = JsonTokensRepository() @@ -65,10 +65,12 @@ def delete_api_token(caller_token: str, token_name: str) -> None: def refresh_api_token(caller_token: str) -> str: """Refresh the token""" - new_token = refresh_token(caller_token) - if new_token is None: + try: + old_token = TOKEN_REPO.get_token_by_token_string(caller_token) + new_token = TOKEN_REPO.refresh_token(old_token) + except TokenNotFound: raise NotFoundException("Token not found") - return new_token + return new_token.token class RecoveryTokenStatus(BaseModel): diff --git a/selfprivacy_api/graphql/mutations/api_mutations.py b/selfprivacy_api/graphql/mutations/api_mutations.py index 3626d83..9f04317 100644 --- a/selfprivacy_api/graphql/mutations/api_mutations.py +++ b/selfprivacy_api/graphql/mutations/api_mutations.py @@ -11,6 +11,7 @@ from selfprivacy_api.actions.api_tokens import ( NotFoundException, delete_api_token, get_new_api_recovery_key, + refresh_api_token, ) from selfprivacy_api.graphql import IsAuthenticated from selfprivacy_api.graphql.mutations.mutation_interface import ( @@ -139,15 +140,14 @@ class ApiMutations: ) try: - old_token = TOKEN_REPO.get_token_by_token_string(token_string) - new_token = TOKEN_REPO.refresh_token(old_token) + new_token = refresh_api_token(token_string) return DeviceApiTokenMutationReturn( success=True, message="Token refreshed", code=200, - token=new_token.token, + token=new_token, ) - except: + except NotFoundException: return DeviceApiTokenMutationReturn( success=False, message="Token not found", diff --git a/selfprivacy_api/utils/auth.py b/selfprivacy_api/utils/auth.py index ecaf9af..1788f8f 100644 --- a/selfprivacy_api/utils/auth.py +++ b/selfprivacy_api/utils/auth.py @@ -145,17 +145,6 @@ def delete_token(token_name): tokens["tokens"] = [t for t in tokens["tokens"] if t["name"] != token_name] -def refresh_token(token: str) -> typing.Optional[str]: - """Change the token field of the existing token""" - new_token = _generate_token() - with WriteUserData(UserDataFiles.TOKENS) as tokens: - for t in tokens["tokens"]: - if t["token"] == token: - t["token"] = new_token - return new_token - return None - - def is_recovery_token_exists(): """Check if recovery token exists""" with ReadUserData(UserDataFiles.TOKENS) as tokens: diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index 1083be5..12de0cf 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -5,6 +5,12 @@ import datetime import pytest from mnemonic import Mnemonic +from selfprivacy_api.repositories.tokens.json_tokens_repository import ( + JsonTokensRepository, +) + +TOKEN_REPO = JsonTokensRepository() + from tests.common import read_json, write_json @@ -97,7 +103,7 @@ def test_refresh_token(authorized_client, tokens_file): response = authorized_client.post("/auth/tokens") assert response.status_code == 200 new_token = response.json()["token"] - assert read_json(tokens_file)["tokens"][0]["token"] == new_token + assert TOKEN_REPO.get_token_by_token_string(new_token) is not None # new device From 20410ec790661cc2d3fb6e86a7cc9bf19faca5f3 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 21 Dec 2022 16:10:41 +0000 Subject: [PATCH 08/34] fix(tokens-repo): fix name pair validation being able to raise a notfound error --- .../repositories/tokens/abstract_tokens_repository.py | 7 +++++-- .../test_repository/test_tokens_repository.py | 8 ++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py index 4f86e61..3b23a84 100644 --- a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py @@ -77,8 +77,11 @@ class AbstractTokensRepository(ABC): def is_token_name_pair_valid(self, token_name: str, token_string: str) -> bool: """Check if the token name and token are valid""" - token = self.get_token_by_name(token_name) - if token is None: + try: + token = self.get_token_by_name(token_name) + if token is None: + return False + except TokenNotFound: return False return token.token == token_string diff --git a/tests/test_graphql/test_repository/test_tokens_repository.py b/tests/test_graphql/test_repository/test_tokens_repository.py index 1a26247..d3650fe 100644 --- a/tests/test_graphql/test_repository/test_tokens_repository.py +++ b/tests/test_graphql/test_repository/test_tokens_repository.py @@ -207,6 +207,14 @@ def test_get_token_by_non_existent_name(some_tokens_repo): assert repo.get_token_by_name(token_name="badname") is None +def test_is_token_name_pair_valid(some_tokens_repo): + repo = some_tokens_repo + token = repo.get_tokens()[0] + assert repo.is_token_name_pair_valid(token.device_name, token.token) + assert not repo.is_token_name_pair_valid(token.device_name, "gibberish") + assert not repo.is_token_name_pair_valid("gibberish", token.token) + + def test_get_tokens(some_tokens_repo): repo = some_tokens_repo tokenstrings = [] From 2a239e35ad3d1cf04cd23c0de569df7f282d5983 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 21 Dec 2022 16:17:56 +0000 Subject: [PATCH 09/34] refactor(tokens-repo): delete is_token_name_pair_valid from auth --- selfprivacy_api/actions/api_tokens.py | 3 +-- selfprivacy_api/utils/auth.py | 9 --------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/selfprivacy_api/actions/api_tokens.py b/selfprivacy_api/actions/api_tokens.py index 38cd208..820d7c6 100644 --- a/selfprivacy_api/actions/api_tokens.py +++ b/selfprivacy_api/actions/api_tokens.py @@ -12,7 +12,6 @@ from selfprivacy_api.utils.auth import ( is_recovery_token_exists, is_recovery_token_valid, is_token_name_exists, - is_token_name_pair_valid, get_token_name, ) @@ -56,7 +55,7 @@ class CannotDeleteCallerException(Exception): def delete_api_token(caller_token: str, token_name: str) -> None: """Delete the token""" - if is_token_name_pair_valid(token_name, caller_token): + if TOKEN_REPO.is_token_name_pair_valid(token_name, caller_token): raise CannotDeleteCallerException("Cannot delete caller's token") if not is_token_name_exists(token_name): raise NotFoundException("Token not found") diff --git a/selfprivacy_api/utils/auth.py b/selfprivacy_api/utils/auth.py index 1788f8f..a6c6299 100644 --- a/selfprivacy_api/utils/auth.py +++ b/selfprivacy_api/utils/auth.py @@ -79,15 +79,6 @@ def is_token_name_exists(token_name): return token_name in [t["name"] for t in tokens["tokens"]] -def is_token_name_pair_valid(token_name, token): - """Check if token name and token pair exists""" - with ReadUserData(UserDataFiles.TOKENS) as tokens: - for t in tokens["tokens"]: - if t["name"] == token_name and t["token"] == token: - return True - return False - - def get_token_name(token: str) -> typing.Optional[str]: """Return the name of the token provided""" with ReadUserData(UserDataFiles.TOKENS) as tokens: From 22a309466e59974f04e85c5076febfb76579d6ad Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 21 Dec 2022 17:04:18 +0000 Subject: [PATCH 10/34] fix(tokens-repo): fix is_name_exists() being fallible --- .../repositories/tokens/abstract_tokens_repository.py | 5 +---- .../test_graphql/test_repository/test_tokens_repository.py | 7 +++++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py index 3b23a84..03c65bd 100644 --- a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py @@ -70,10 +70,7 @@ class AbstractTokensRepository(ABC): def is_token_name_exists(self, token_name: str) -> bool: """Check if the token name exists""" - token = self.get_token_by_name(token_name) - if token is None: - return False - return True + return token_name in [token.device_name for token in self.get_tokens()] def is_token_name_pair_valid(self, token_name: str, token_string: str) -> bool: """Check if the token name and token are valid""" diff --git a/tests/test_graphql/test_repository/test_tokens_repository.py b/tests/test_graphql/test_repository/test_tokens_repository.py index d3650fe..95ba7c0 100644 --- a/tests/test_graphql/test_repository/test_tokens_repository.py +++ b/tests/test_graphql/test_repository/test_tokens_repository.py @@ -215,6 +215,13 @@ def test_is_token_name_pair_valid(some_tokens_repo): assert not repo.is_token_name_pair_valid("gibberish", token.token) +def test_is_token_name_exists(some_tokens_repo): + repo = some_tokens_repo + token = repo.get_tokens()[0] + assert repo.is_token_name_exists(token.device_name) + assert not repo.is_token_name_exists("gibberish") + + def test_get_tokens(some_tokens_repo): repo = some_tokens_repo tokenstrings = [] From 5a1f64b1e7b06bfdf6e03be8c8b355b3e4c4e781 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 21 Dec 2022 17:09:49 +0000 Subject: [PATCH 11/34] refactor(tokens-repo): delete is_name_exists() from auth utils --- selfprivacy_api/actions/api_tokens.py | 3 +-- selfprivacy_api/utils/auth.py | 6 ------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/selfprivacy_api/actions/api_tokens.py b/selfprivacy_api/actions/api_tokens.py index 820d7c6..8044a76 100644 --- a/selfprivacy_api/actions/api_tokens.py +++ b/selfprivacy_api/actions/api_tokens.py @@ -11,7 +11,6 @@ from selfprivacy_api.utils.auth import ( get_tokens_info, is_recovery_token_exists, is_recovery_token_valid, - is_token_name_exists, get_token_name, ) @@ -57,7 +56,7 @@ def delete_api_token(caller_token: str, token_name: str) -> None: """Delete the token""" if TOKEN_REPO.is_token_name_pair_valid(token_name, caller_token): raise CannotDeleteCallerException("Cannot delete caller's token") - if not is_token_name_exists(token_name): + if not TOKEN_REPO.is_token_name_exists(token_name): raise NotFoundException("Token not found") delete_token(token_name) diff --git a/selfprivacy_api/utils/auth.py b/selfprivacy_api/utils/auth.py index a6c6299..1d6939e 100644 --- a/selfprivacy_api/utils/auth.py +++ b/selfprivacy_api/utils/auth.py @@ -73,12 +73,6 @@ def is_token_valid(token): return False -def is_token_name_exists(token_name): - """Check if token name exists""" - with ReadUserData(UserDataFiles.TOKENS) as tokens: - return token_name in [t["name"] for t in tokens["tokens"]] - - def get_token_name(token: str) -> typing.Optional[str]: """Return the name of the token provided""" with ReadUserData(UserDataFiles.TOKENS) as tokens: From a2ac47b0f5604b0e49274840e1598ca071e81036 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 23 Dec 2022 11:14:52 +0000 Subject: [PATCH 12/34] refactor(tokens-repo): delete delete_token from auth utils --- selfprivacy_api/actions/api_tokens.py | 4 ++-- selfprivacy_api/utils/auth.py | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/selfprivacy_api/actions/api_tokens.py b/selfprivacy_api/actions/api_tokens.py index 8044a76..bd1d4fe 100644 --- a/selfprivacy_api/actions/api_tokens.py +++ b/selfprivacy_api/actions/api_tokens.py @@ -6,7 +6,6 @@ from mnemonic import Mnemonic from selfprivacy_api.utils.auth import ( - delete_token, get_recovery_token_status, get_tokens_info, is_recovery_token_exists, @@ -58,7 +57,8 @@ def delete_api_token(caller_token: str, token_name: str) -> None: raise CannotDeleteCallerException("Cannot delete caller's token") if not TOKEN_REPO.is_token_name_exists(token_name): raise NotFoundException("Token not found") - delete_token(token_name) + token = TOKEN_REPO.get_token_by_name(token_name) + TOKEN_REPO.delete_token(token) def refresh_api_token(caller_token: str) -> str: diff --git a/selfprivacy_api/utils/auth.py b/selfprivacy_api/utils/auth.py index 1d6939e..e1c3b7e 100644 --- a/selfprivacy_api/utils/auth.py +++ b/selfprivacy_api/utils/auth.py @@ -124,12 +124,6 @@ def create_token(name): return token -def delete_token(token_name): - """Delete token""" - with WriteUserData(UserDataFiles.TOKENS) as tokens: - tokens["tokens"] = [t for t in tokens["tokens"] if t["name"] != token_name] - - def is_recovery_token_exists(): """Check if recovery token exists""" with ReadUserData(UserDataFiles.TOKENS) as tokens: From f928ca160a843a22eb1aec79b3156334fc780631 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 23 Dec 2022 12:00:35 +0000 Subject: [PATCH 13/34] refactor(tokens-repo): use token repo in get_api_recovery_token_status action --- selfprivacy_api/actions/api_tokens.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/selfprivacy_api/actions/api_tokens.py b/selfprivacy_api/actions/api_tokens.py index bd1d4fe..399452a 100644 --- a/selfprivacy_api/actions/api_tokens.py +++ b/selfprivacy_api/actions/api_tokens.py @@ -6,10 +6,7 @@ from mnemonic import Mnemonic from selfprivacy_api.utils.auth import ( - get_recovery_token_status, get_tokens_info, - is_recovery_token_exists, - is_recovery_token_valid, get_token_name, ) @@ -83,18 +80,16 @@ class RecoveryTokenStatus(BaseModel): def get_api_recovery_token_status() -> RecoveryTokenStatus: """Get the recovery token status""" - if not is_recovery_token_exists(): + token = TOKEN_REPO.get_recovery_key() + if token is None: return RecoveryTokenStatus(exists=False, valid=False) - status = get_recovery_token_status() - if status is None: - return RecoveryTokenStatus(exists=False, valid=False) - is_valid = is_recovery_token_valid() + is_valid = TOKEN_REPO.is_recovery_key_valid() return RecoveryTokenStatus( exists=True, valid=is_valid, - date=status["date"], - expiration=status["expiration"], - uses_left=status["uses_left"], + date=token.created_at, + expiration=token.expires_at, + uses_left=token.uses_left, ) From e817de6228c60a6a9e9ace7015e63e476b9e9523 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 23 Dec 2022 12:29:32 +0000 Subject: [PATCH 14/34] refactor(tokens-repo): use token repo in get_api_tokens_with_caller_flag --- selfprivacy_api/actions/api_tokens.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/selfprivacy_api/actions/api_tokens.py b/selfprivacy_api/actions/api_tokens.py index 399452a..03de653 100644 --- a/selfprivacy_api/actions/api_tokens.py +++ b/selfprivacy_api/actions/api_tokens.py @@ -4,12 +4,6 @@ from typing import Optional from pydantic import BaseModel from mnemonic import Mnemonic - -from selfprivacy_api.utils.auth import ( - get_tokens_info, - get_token_name, -) - from selfprivacy_api.repositories.tokens.json_tokens_repository import ( JsonTokensRepository, ) @@ -28,13 +22,13 @@ class TokenInfoWithIsCaller(BaseModel): def get_api_tokens_with_caller_flag(caller_token: str) -> list[TokenInfoWithIsCaller]: """Get the tokens info""" - caller_name = get_token_name(caller_token) - tokens = get_tokens_info() + caller_name = TOKEN_REPO.get_token_by_token_string(caller_token).device_name + tokens = TOKEN_REPO.get_tokens() return [ TokenInfoWithIsCaller( - name=token.name, - date=token.date, - is_caller=token.name == caller_name, + name=token.device_name, + date=token.created_at, + is_caller=token.device_name == caller_name, ) for token in tokens ] From 5dedbda41f2183d43620ac35d2013b267073c80c Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 23 Dec 2022 12:41:29 +0000 Subject: [PATCH 15/34] refactor(tokens-repo): delete get_tokens_info() from auth utils --- selfprivacy_api/utils/auth.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/selfprivacy_api/utils/auth.py b/selfprivacy_api/utils/auth.py index e1c3b7e..3383f8d 100644 --- a/selfprivacy_api/utils/auth.py +++ b/selfprivacy_api/utils/auth.py @@ -89,18 +89,6 @@ class BasicTokenInfo(BaseModel): date: datetime -def get_tokens_info(): - """Get all tokens info without tokens themselves""" - with ReadUserData(UserDataFiles.TOKENS) as tokens: - return [ - BasicTokenInfo( - name=t["name"], - date=parse_date(t["date"]), - ) - for t in tokens["tokens"] - ] - - def _generate_token(): """Generates new token and makes sure it is unique""" token = secrets.token_urlsafe(32) From 3f6aa9bd06876707e47a2ed20816d095b6c3d7e9 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 26 Dec 2022 10:13:37 +0000 Subject: [PATCH 16/34] refactor(tokens-repo): delete delete_new_device_auth_token from auth utils --- selfprivacy_api/actions/api_tokens.py | 4 ++++ selfprivacy_api/graphql/mutations/api_mutations.py | 2 +- selfprivacy_api/rest/api_auth.py | 2 +- selfprivacy_api/utils/auth.py | 7 ------- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/selfprivacy_api/actions/api_tokens.py b/selfprivacy_api/actions/api_tokens.py index 03de653..a90aa12 100644 --- a/selfprivacy_api/actions/api_tokens.py +++ b/selfprivacy_api/actions/api_tokens.py @@ -110,3 +110,7 @@ def get_new_api_recovery_key( key = TOKEN_REPO.create_recovery_key(expiration_date, uses_left) mnemonic_phrase = Mnemonic(language="english").to_mnemonic(bytes.fromhex(key.key)) return mnemonic_phrase + + +def delete_new_device_auth_token() -> None: + TOKEN_REPO.delete_new_device_key() diff --git a/selfprivacy_api/graphql/mutations/api_mutations.py b/selfprivacy_api/graphql/mutations/api_mutations.py index 9f04317..0c83eab 100644 --- a/selfprivacy_api/graphql/mutations/api_mutations.py +++ b/selfprivacy_api/graphql/mutations/api_mutations.py @@ -12,6 +12,7 @@ from selfprivacy_api.actions.api_tokens import ( delete_api_token, get_new_api_recovery_key, refresh_api_token, + delete_new_device_auth_token, ) from selfprivacy_api.graphql import IsAuthenticated from selfprivacy_api.graphql.mutations.mutation_interface import ( @@ -20,7 +21,6 @@ from selfprivacy_api.graphql.mutations.mutation_interface import ( ) from selfprivacy_api.utils.auth import ( - delete_new_device_auth_token, get_new_device_auth_token, use_new_device_auth_token, ) diff --git a/selfprivacy_api/rest/api_auth.py b/selfprivacy_api/rest/api_auth.py index f73056c..8209ef6 100644 --- a/selfprivacy_api/rest/api_auth.py +++ b/selfprivacy_api/rest/api_auth.py @@ -12,12 +12,12 @@ from selfprivacy_api.actions.api_tokens import ( get_api_tokens_with_caller_flag, get_new_api_recovery_key, refresh_api_token, + delete_new_device_auth_token, ) from selfprivacy_api.dependencies import TokenHeader, get_token_header from selfprivacy_api.utils.auth import ( - delete_new_device_auth_token, get_new_device_auth_token, use_mnemonic_recoverery_token, use_new_device_auth_token, diff --git a/selfprivacy_api/utils/auth.py b/selfprivacy_api/utils/auth.py index 3383f8d..847cd30 100644 --- a/selfprivacy_api/utils/auth.py +++ b/selfprivacy_api/utils/auth.py @@ -257,13 +257,6 @@ def _get_new_device_auth_token(): return new_device["token"] -def delete_new_device_auth_token(): - """Delete new device auth token""" - with WriteUserData(UserDataFiles.TOKENS) as tokens: - if "new_device" in tokens: - del tokens["new_device"] - - def use_new_device_auth_token(mnemonic_phrase, name): """Use the new device auth token by converting the mnemonic string to a byte array. If the mnemonic phrase is valid then generate a device token and return it. From cb1906144c22be4b363e9ca874558e3d1b5d214d Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 26 Dec 2022 10:27:10 +0000 Subject: [PATCH 17/34] refactor(tokens-repo): delete get_new_device_auth_token from auth utils --- selfprivacy_api/actions/api_tokens.py | 8 ++++++++ .../graphql/mutations/api_mutations.py | 2 +- selfprivacy_api/rest/api_auth.py | 2 +- selfprivacy_api/utils/auth.py | 16 ---------------- 4 files changed, 10 insertions(+), 18 deletions(-) diff --git a/selfprivacy_api/actions/api_tokens.py b/selfprivacy_api/actions/api_tokens.py index a90aa12..b13c13e 100644 --- a/selfprivacy_api/actions/api_tokens.py +++ b/selfprivacy_api/actions/api_tokens.py @@ -114,3 +114,11 @@ def get_new_api_recovery_key( def delete_new_device_auth_token() -> None: TOKEN_REPO.delete_new_device_key() + + +def get_new_device_auth_token() -> str: + """Generate and store a new device auth token which is valid for 10 minutes + and return a mnemonic phrase representation + """ + key = TOKEN_REPO.get_new_device_key() + return Mnemonic(language="english").to_mnemonic(bytes.fromhex(key.key)) diff --git a/selfprivacy_api/graphql/mutations/api_mutations.py b/selfprivacy_api/graphql/mutations/api_mutations.py index 0c83eab..c2075c3 100644 --- a/selfprivacy_api/graphql/mutations/api_mutations.py +++ b/selfprivacy_api/graphql/mutations/api_mutations.py @@ -13,6 +13,7 @@ from selfprivacy_api.actions.api_tokens import ( get_new_api_recovery_key, refresh_api_token, delete_new_device_auth_token, + get_new_device_auth_token, ) from selfprivacy_api.graphql import IsAuthenticated from selfprivacy_api.graphql.mutations.mutation_interface import ( @@ -21,7 +22,6 @@ from selfprivacy_api.graphql.mutations.mutation_interface import ( ) from selfprivacy_api.utils.auth import ( - get_new_device_auth_token, use_new_device_auth_token, ) diff --git a/selfprivacy_api/rest/api_auth.py b/selfprivacy_api/rest/api_auth.py index 8209ef6..a860798 100644 --- a/selfprivacy_api/rest/api_auth.py +++ b/selfprivacy_api/rest/api_auth.py @@ -13,12 +13,12 @@ from selfprivacy_api.actions.api_tokens import ( get_new_api_recovery_key, refresh_api_token, delete_new_device_auth_token, + get_new_device_auth_token, ) from selfprivacy_api.dependencies import TokenHeader, get_token_header from selfprivacy_api.utils.auth import ( - get_new_device_auth_token, use_mnemonic_recoverery_token, use_new_device_auth_token, ) diff --git a/selfprivacy_api/utils/auth.py b/selfprivacy_api/utils/auth.py index 847cd30..f27a826 100644 --- a/selfprivacy_api/utils/auth.py +++ b/selfprivacy_api/utils/auth.py @@ -227,22 +227,6 @@ def use_mnemonic_recoverery_token(mnemonic_phrase, name): return token -def get_new_device_auth_token() -> str: - """Generate a new device auth token which is valid for 10 minutes - and return a mnemonic phrase representation - Write token to the new_device of the tokens.json file. - """ - token = secrets.token_bytes(16) - token_str = token.hex() - with WriteUserData(UserDataFiles.TOKENS) as tokens: - tokens["new_device"] = { - "token": token_str, - "date": str(datetime.now()), - "expiration": str(datetime.now() + timedelta(minutes=10)), - } - return Mnemonic(language="english").to_mnemonic(token) - - def _get_new_device_auth_token(): """Get new device auth token. If it is expired, return None""" with ReadUserData(UserDataFiles.TOKENS) as tokens: From 29723b9f3a1d028bbe10da26b6694d36ce4c6aec Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 26 Dec 2022 10:53:27 +0000 Subject: [PATCH 18/34] refactor(tokens-repo): delete use_mnemonic_recoverery_token from auth utils --- selfprivacy_api/actions/api_tokens.py | 22 ++++++++++- .../graphql/mutations/api_mutations.py | 9 +++-- selfprivacy_api/rest/api_auth.py | 6 +-- selfprivacy_api/utils/auth.py | 39 ------------------- 4 files changed, 29 insertions(+), 47 deletions(-) diff --git a/selfprivacy_api/actions/api_tokens.py b/selfprivacy_api/actions/api_tokens.py index b13c13e..394d3d9 100644 --- a/selfprivacy_api/actions/api_tokens.py +++ b/selfprivacy_api/actions/api_tokens.py @@ -7,7 +7,11 @@ from mnemonic import Mnemonic from selfprivacy_api.repositories.tokens.json_tokens_repository import ( JsonTokensRepository, ) -from selfprivacy_api.repositories.tokens.exceptions import TokenNotFound +from selfprivacy_api.repositories.tokens.exceptions import ( + TokenNotFound, + RecoveryKeyNotFound, + InvalidMnemonic, +) TOKEN_REPO = JsonTokensRepository() @@ -112,6 +116,22 @@ def get_new_api_recovery_key( return mnemonic_phrase +def use_mnemonic_recovery_token(mnemonic_phrase, name): + """Use the recovery token by converting the mnemonic word list to a byte array. + If the recovery token if invalid itself, return None + If the binary representation of phrase not matches + the byte array of the recovery token, return None. + If the mnemonic phrase is valid then generate a device token and return it. + Substract 1 from uses_left if it exists. + mnemonic_phrase is a string representation of the mnemonic word list. + """ + try: + token = TOKEN_REPO.use_mnemonic_recovery_key(mnemonic_phrase, name) + return token.token + except (RecoveryKeyNotFound, InvalidMnemonic): + return None + + def delete_new_device_auth_token() -> None: TOKEN_REPO.delete_new_device_key() diff --git a/selfprivacy_api/graphql/mutations/api_mutations.py b/selfprivacy_api/graphql/mutations/api_mutations.py index c2075c3..45961c8 100644 --- a/selfprivacy_api/graphql/mutations/api_mutations.py +++ b/selfprivacy_api/graphql/mutations/api_mutations.py @@ -11,6 +11,7 @@ from selfprivacy_api.actions.api_tokens import ( NotFoundException, delete_api_token, get_new_api_recovery_key, + use_mnemonic_recovery_token, refresh_api_token, delete_new_device_auth_token, get_new_device_auth_token, @@ -107,15 +108,15 @@ class ApiMutations: self, input: UseRecoveryKeyInput ) -> DeviceApiTokenMutationReturn: """Use recovery key""" - try: - token = TOKEN_REPO.use_mnemonic_recovery_key(input.key, input.deviceName) + token = use_mnemonic_recovery_token(input.key, input.deviceName) + if token is not None: return DeviceApiTokenMutationReturn( success=True, message="Recovery key used", code=200, - token=token.token, + token=token, ) - except (RecoveryKeyNotFound, InvalidMnemonic): + else: return DeviceApiTokenMutationReturn( success=False, message="Recovery key not found", diff --git a/selfprivacy_api/rest/api_auth.py b/selfprivacy_api/rest/api_auth.py index a860798..ab96bee 100644 --- a/selfprivacy_api/rest/api_auth.py +++ b/selfprivacy_api/rest/api_auth.py @@ -8,10 +8,11 @@ from selfprivacy_api.actions.api_tokens import ( InvalidUsesLeft, NotFoundException, delete_api_token, + refresh_api_token, get_api_recovery_token_status, get_api_tokens_with_caller_flag, get_new_api_recovery_key, - refresh_api_token, + use_mnemonic_recovery_token, delete_new_device_auth_token, get_new_device_auth_token, ) @@ -19,7 +20,6 @@ from selfprivacy_api.actions.api_tokens import ( from selfprivacy_api.dependencies import TokenHeader, get_token_header from selfprivacy_api.utils.auth import ( - use_mnemonic_recoverery_token, use_new_device_auth_token, ) @@ -99,7 +99,7 @@ class UseTokenInput(BaseModel): @router.post("/recovery_token/use") async def rest_use_recovery_token(input: UseTokenInput): - token = use_mnemonic_recoverery_token(input.token, input.device) + token = use_mnemonic_recovery_token(input.token, input.device) if token is None: raise HTTPException(status_code=404, detail="Token not found") return {"token": token} diff --git a/selfprivacy_api/utils/auth.py b/selfprivacy_api/utils/auth.py index f27a826..11836b1 100644 --- a/selfprivacy_api/utils/auth.py +++ b/selfprivacy_api/utils/auth.py @@ -188,45 +188,6 @@ def generate_recovery_token( return Mnemonic(language="english").to_mnemonic(recovery_token) -def use_mnemonic_recoverery_token(mnemonic_phrase, name): - """Use the recovery token by converting the mnemonic word list to a byte array. - If the recovery token if invalid itself, return None - If the binary representation of phrase not matches - the byte array of the recovery token, return None. - If the mnemonic phrase is valid then generate a device token and return it. - Substract 1 from uses_left if it exists. - mnemonic_phrase is a string representation of the mnemonic word list. - """ - if not is_recovery_token_valid(): - return None - recovery_token_str = _get_recovery_token() - if recovery_token_str is None: - return None - recovery_token = bytes.fromhex(recovery_token_str) - if not Mnemonic(language="english").check(mnemonic_phrase): - return None - phrase_bytes = Mnemonic(language="english").to_entropy(mnemonic_phrase) - if phrase_bytes != recovery_token: - return None - token = _generate_token() - name = _validate_token_name(name) - with WriteUserData(UserDataFiles.TOKENS) as tokens: - tokens["tokens"].append( - { - "token": token, - "name": name, - "date": str(datetime.now()), - } - ) - if "recovery_token" in tokens: - if ( - "uses_left" in tokens["recovery_token"] - and tokens["recovery_token"]["uses_left"] is not None - ): - tokens["recovery_token"]["uses_left"] -= 1 - return token - - def _get_new_device_auth_token(): """Get new device auth token. If it is expired, return None""" with ReadUserData(UserDataFiles.TOKENS) as tokens: From c037a12f4d2a882b2a29de1f1ef6be679d079be7 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 26 Dec 2022 12:31:09 +0000 Subject: [PATCH 19/34] refactor(tokens-repo): break out _store_new_device_key() --- .../repositories/tokens/abstract_tokens_repository.py | 8 +++++++- .../repositories/tokens/json_tokens_repository.py | 7 +------ .../repositories/tokens/redis_tokens_repository.py | 6 ++---- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py index 03c65bd..bee76b7 100644 --- a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py @@ -118,9 +118,15 @@ class AbstractTokensRepository(ABC): return False return recovery_key.is_valid() - @abstractmethod def get_new_device_key(self) -> NewDeviceKey: """Creates and returns the new device key""" + new_device_key = NewDeviceKey.generate() + self._store_new_device_key(new_device_key) + + return new_device_key + + def _store_new_device_key(self, new_device_key: NewDeviceKey) -> None: + """Store new device key directly""" @abstractmethod def delete_new_device_key(self) -> None: diff --git a/selfprivacy_api/repositories/tokens/json_tokens_repository.py b/selfprivacy_api/repositories/tokens/json_tokens_repository.py index e43a3eb..394c046 100644 --- a/selfprivacy_api/repositories/tokens/json_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/json_tokens_repository.py @@ -104,10 +104,7 @@ class JsonTokensRepository(AbstractTokensRepository): if tokens["recovery_token"]["uses_left"] is not None: tokens["recovery_token"]["uses_left"] -= 1 - def get_new_device_key(self) -> NewDeviceKey: - """Creates and returns the new device key""" - new_device_key = NewDeviceKey.generate() - + def _store_new_device_key(self, new_device_key: NewDeviceKey) -> None: with WriteUserData(UserDataFiles.TOKENS) as tokens_file: tokens_file["new_device"] = { "token": new_device_key.key, @@ -115,8 +112,6 @@ class JsonTokensRepository(AbstractTokensRepository): "expiration": new_device_key.expires_at.strftime(DATETIME_FORMAT), } - return new_device_key - def delete_new_device_key(self) -> None: """Delete the new device key""" with WriteUserData(UserDataFiles.TOKENS) as tokens_file: diff --git a/selfprivacy_api/repositories/tokens/redis_tokens_repository.py b/selfprivacy_api/repositories/tokens/redis_tokens_repository.py index 2fb6180..d665553 100644 --- a/selfprivacy_api/repositories/tokens/redis_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/redis_tokens_repository.py @@ -68,11 +68,9 @@ class RedisTokensRepository(AbstractTokensRepository): self._store_model_as_hash(RECOVERY_KEY_REDIS_KEY, recovery_key) return recovery_key - def get_new_device_key(self) -> NewDeviceKey: - """Creates and returns the new device key""" - new_device_key = NewDeviceKey.generate() + def _store_new_device_key(self, new_device_key: NewDeviceKey) -> None: + """Store new device key directly""" self._store_model_as_hash(NEW_DEVICE_KEY_REDIS_KEY, new_device_key) - return new_device_key def delete_new_device_key(self) -> None: """Delete the new device key""" From 5d4ed7343542b0d61392b82200ed92b0480ac4f8 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 26 Dec 2022 14:01:36 +0000 Subject: [PATCH 20/34] fix(tokens-repo): properly expire new device keys --- .../tokens/abstract_tokens_repository.py | 3 ++ .../test_repository/test_tokens_repository.py | 37 ++++++++++++++++--- tests/test_models.py | 8 ++++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py index bee76b7..7601ef4 100644 --- a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py @@ -140,6 +140,9 @@ class AbstractTokensRepository(ABC): if not new_device_key: raise NewDeviceKeyNotFound + if not new_device_key.is_valid(): + raise NewDeviceKeyNotFound + if not self._assert_mnemonic(new_device_key.key, mnemonic_phrase): raise NewDeviceKeyNotFound("Phrase is not token!") diff --git a/tests/test_graphql/test_repository/test_tokens_repository.py b/tests/test_graphql/test_repository/test_tokens_repository.py index 95ba7c0..a95878d 100644 --- a/tests/test_graphql/test_repository/test_tokens_repository.py +++ b/tests/test_graphql/test_repository/test_tokens_repository.py @@ -513,15 +513,20 @@ def test_use_not_exists_mnemonic_new_device_key( ) -def test_use_mnemonic_new_device_key( - empty_repo, mock_new_device_key_generate_for_mnemonic -): +def mnemonic_from_hex(hexkey): + return Mnemonic(language="english").to_mnemonic(bytes.fromhex(hexkey)) + + +def test_use_mnemonic_new_device_key(empty_repo): repo = empty_repo - assert repo.get_new_device_key() is not None + key = repo.get_new_device_key() + assert key is not None + + mnemonic_phrase = mnemonic_from_hex(key.key) new_token = repo.use_mnemonic_new_device_key( device_name="imnew", - mnemonic_phrase="captain ribbon toddler settle symbol minute step broccoli bless universe divide bulb", + mnemonic_phrase=mnemonic_phrase, ) assert new_token.device_name == "imnew" @@ -532,12 +537,32 @@ def test_use_mnemonic_new_device_key( assert ( repo.use_mnemonic_new_device_key( device_name="imnew", - mnemonic_phrase="captain ribbon toddler settle symbol minute step broccoli bless universe divide bulb", + mnemonic_phrase=mnemonic_phrase, ) is None ) +def test_use_mnemonic_expired_new_device_key( + some_tokens_repo, +): + repo = some_tokens_repo + expiration = datetime.now() - timedelta(minutes=5) + + key = repo.get_new_device_key() + assert key is not None + assert key.expires_at is not None + key.expires_at = expiration + assert not key.is_valid() + repo._store_new_device_key(key) + + with pytest.raises(NewDeviceKeyNotFound): + token = repo.use_mnemonic_new_device_key( + mnemonic_phrase=mnemonic_from_hex(key.key), + device_name="imnew", + ) + + def test_use_mnemonic_new_device_key_when_empty(empty_repo): repo = empty_repo diff --git a/tests/test_models.py b/tests/test_models.py index 078dfd5..2263e82 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -2,9 +2,17 @@ import pytest from datetime import datetime, timedelta from selfprivacy_api.models.tokens.recovery_key import RecoveryKey +from selfprivacy_api.models.tokens.new_device_key import NewDeviceKey def test_recovery_key_expired(): expiration = datetime.now() - timedelta(minutes=5) key = RecoveryKey.generate(expiration=expiration, uses_left=2) assert not key.is_valid() + + +def test_new_device_key_expired(): + expiration = datetime.now() - timedelta(minutes=5) + key = NewDeviceKey.generate() + key.expires_at = expiration + assert not key.is_valid() From f33d5155b04840f5c2ebe3e830d017fb09f311f7 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 26 Dec 2022 14:03:55 +0000 Subject: [PATCH 21/34] refactor(tokens-repo): delete use_new_device_auth_token from auth utils --- selfprivacy_api/actions/api_tokens.py | 13 ++++++++++++ .../graphql/mutations/api_mutations.py | 4 +--- selfprivacy_api/rest/api_auth.py | 4 +--- selfprivacy_api/utils/auth.py | 21 ------------------- 4 files changed, 15 insertions(+), 27 deletions(-) diff --git a/selfprivacy_api/actions/api_tokens.py b/selfprivacy_api/actions/api_tokens.py index 394d3d9..3b180e8 100644 --- a/selfprivacy_api/actions/api_tokens.py +++ b/selfprivacy_api/actions/api_tokens.py @@ -11,6 +11,7 @@ from selfprivacy_api.repositories.tokens.exceptions import ( TokenNotFound, RecoveryKeyNotFound, InvalidMnemonic, + NewDeviceKeyNotFound, ) TOKEN_REPO = JsonTokensRepository() @@ -142,3 +143,15 @@ def get_new_device_auth_token() -> str: """ key = TOKEN_REPO.get_new_device_key() return Mnemonic(language="english").to_mnemonic(bytes.fromhex(key.key)) + + +def use_new_device_auth_token(mnemonic_phrase, name) -> str: + """Use the new device auth token by converting the mnemonic string to a byte array. + If the mnemonic phrase is valid then generate a device token and return it. + New device auth token must be deleted. + """ + try: + token = TOKEN_REPO.use_mnemonic_new_device_key(mnemonic_phrase, name) + return token.token + except (NewDeviceKeyNotFound, InvalidMnemonic): + return None diff --git a/selfprivacy_api/graphql/mutations/api_mutations.py b/selfprivacy_api/graphql/mutations/api_mutations.py index 45961c8..0c413fb 100644 --- a/selfprivacy_api/graphql/mutations/api_mutations.py +++ b/selfprivacy_api/graphql/mutations/api_mutations.py @@ -15,6 +15,7 @@ from selfprivacy_api.actions.api_tokens import ( refresh_api_token, delete_new_device_auth_token, get_new_device_auth_token, + use_new_device_auth_token, ) from selfprivacy_api.graphql import IsAuthenticated from selfprivacy_api.graphql.mutations.mutation_interface import ( @@ -22,9 +23,6 @@ from selfprivacy_api.graphql.mutations.mutation_interface import ( MutationReturnInterface, ) -from selfprivacy_api.utils.auth import ( - use_new_device_auth_token, -) from selfprivacy_api.repositories.tokens.json_tokens_repository import ( JsonTokensRepository, diff --git a/selfprivacy_api/rest/api_auth.py b/selfprivacy_api/rest/api_auth.py index ab96bee..275dac3 100644 --- a/selfprivacy_api/rest/api_auth.py +++ b/selfprivacy_api/rest/api_auth.py @@ -15,13 +15,11 @@ from selfprivacy_api.actions.api_tokens import ( use_mnemonic_recovery_token, delete_new_device_auth_token, get_new_device_auth_token, + use_new_device_auth_token, ) from selfprivacy_api.dependencies import TokenHeader, get_token_header -from selfprivacy_api.utils.auth import ( - use_new_device_auth_token, -) router = APIRouter( prefix="/auth", diff --git a/selfprivacy_api/utils/auth.py b/selfprivacy_api/utils/auth.py index 11836b1..53dffd7 100644 --- a/selfprivacy_api/utils/auth.py +++ b/selfprivacy_api/utils/auth.py @@ -200,24 +200,3 @@ def _get_new_device_auth_token(): if datetime.now() > expiration: return None return new_device["token"] - - -def use_new_device_auth_token(mnemonic_phrase, name): - """Use the new device auth token by converting the mnemonic string to a byte array. - If the mnemonic phrase is valid then generate a device token and return it. - New device auth token must be deleted. - """ - token_str = _get_new_device_auth_token() - if token_str is None: - return None - token = bytes.fromhex(token_str) - if not Mnemonic(language="english").check(mnemonic_phrase): - return None - phrase_bytes = Mnemonic(language="english").to_entropy(mnemonic_phrase) - if phrase_bytes != token: - return None - token = create_token(name) - with WriteUserData(UserDataFiles.TOKENS) as tokens: - if "new_device" in tokens: - del tokens["new_device"] - return token From 7cf295450b048b08c5fa0333cbe27cd6d9f0660f Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 26 Dec 2022 14:13:46 +0000 Subject: [PATCH 22/34] refactor(tokens-repo): do not use tokens repo directly from graphql --- selfprivacy_api/graphql/mutations/api_mutations.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/selfprivacy_api/graphql/mutations/api_mutations.py b/selfprivacy_api/graphql/mutations/api_mutations.py index 0c413fb..49c49ad 100644 --- a/selfprivacy_api/graphql/mutations/api_mutations.py +++ b/selfprivacy_api/graphql/mutations/api_mutations.py @@ -24,17 +24,6 @@ from selfprivacy_api.graphql.mutations.mutation_interface import ( ) -from selfprivacy_api.repositories.tokens.json_tokens_repository import ( - JsonTokensRepository, -) -from selfprivacy_api.repositories.tokens.exceptions import ( - RecoveryKeyNotFound, - InvalidMnemonic, -) - -TOKEN_REPO = JsonTokensRepository() - - @strawberry.type class ApiKeyMutationReturn(MutationReturnInterface): key: typing.Optional[str] From 69577c2854d334cf3bfb9be566aa032615ad1285 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 26 Dec 2022 14:29:05 +0000 Subject: [PATCH 23/34] refactor(tokens-repo): delete get_recovery_key_status from auth utils --- selfprivacy_api/graphql/queries/api_queries.py | 17 ++++++++--------- selfprivacy_api/utils/auth.py | 17 ----------------- 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/selfprivacy_api/graphql/queries/api_queries.py b/selfprivacy_api/graphql/queries/api_queries.py index 7994a8f..7cf7c3e 100644 --- a/selfprivacy_api/graphql/queries/api_queries.py +++ b/selfprivacy_api/graphql/queries/api_queries.py @@ -4,13 +4,14 @@ import datetime import typing import strawberry from strawberry.types import Info -from selfprivacy_api.actions.api_tokens import get_api_tokens_with_caller_flag +from selfprivacy_api.actions.api_tokens import ( + get_api_tokens_with_caller_flag, + get_api_recovery_token_status, +) from selfprivacy_api.graphql import IsAuthenticated -from selfprivacy_api.utils import parse_date from selfprivacy_api.dependencies import get_api_version as get_api_version_dependency from selfprivacy_api.utils.auth import ( - get_recovery_token_status, is_recovery_token_exists, is_recovery_token_valid, ) @@ -51,7 +52,7 @@ def get_recovery_key_status() -> ApiRecoveryKeyStatus: expiration_date=None, uses_left=None, ) - status = get_recovery_token_status() + status = get_api_recovery_token_status() if status is None: return ApiRecoveryKeyStatus( exists=False, @@ -63,11 +64,9 @@ def get_recovery_key_status() -> ApiRecoveryKeyStatus: return ApiRecoveryKeyStatus( exists=True, valid=is_recovery_token_valid(), - creation_date=parse_date(status["date"]), - expiration_date=parse_date(status["expiration"]) - if status["expiration"] is not None - else None, - uses_left=status["uses_left"] if status["uses_left"] is not None else None, + creation_date=status.date, + expiration_date=status.expiration, + uses_left=status.uses_left, ) diff --git a/selfprivacy_api/utils/auth.py b/selfprivacy_api/utils/auth.py index 53dffd7..53df508 100644 --- a/selfprivacy_api/utils/auth.py +++ b/selfprivacy_api/utils/auth.py @@ -132,23 +132,6 @@ def is_recovery_token_valid(): return datetime.now() < parse_date(recovery_token["expiration"]) -def get_recovery_token_status(): - """Get recovery token date of creation, expiration and uses left""" - with ReadUserData(UserDataFiles.TOKENS) as tokens: - if "recovery_token" not in tokens: - return None - recovery_token = tokens["recovery_token"] - return { - "date": recovery_token["date"], - "expiration": recovery_token["expiration"] - if "expiration" in recovery_token - else None, - "uses_left": recovery_token["uses_left"] - if "uses_left" in recovery_token - else None, - } - - def _get_recovery_token(): """Get recovery token""" with ReadUserData(UserDataFiles.TOKENS) as tokens: From b11e5a5f77dfa0d8af54581a18868aa097a0c528 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 26 Dec 2022 14:51:37 +0000 Subject: [PATCH 24/34] refactor(tokens-repo): delete recovery key quieries from auth utils --- .../graphql/queries/api_queries.py | 17 ++-------------- selfprivacy_api/utils/auth.py | 20 ------------------- 2 files changed, 2 insertions(+), 35 deletions(-) diff --git a/selfprivacy_api/graphql/queries/api_queries.py b/selfprivacy_api/graphql/queries/api_queries.py index 7cf7c3e..cf56231 100644 --- a/selfprivacy_api/graphql/queries/api_queries.py +++ b/selfprivacy_api/graphql/queries/api_queries.py @@ -11,11 +11,6 @@ from selfprivacy_api.actions.api_tokens import ( from selfprivacy_api.graphql import IsAuthenticated from selfprivacy_api.dependencies import get_api_version as get_api_version_dependency -from selfprivacy_api.utils.auth import ( - is_recovery_token_exists, - is_recovery_token_valid, -) - def get_api_version() -> str: """Get API version""" @@ -44,16 +39,8 @@ class ApiRecoveryKeyStatus: def get_recovery_key_status() -> ApiRecoveryKeyStatus: """Get recovery key status""" - if not is_recovery_token_exists(): - return ApiRecoveryKeyStatus( - exists=False, - valid=False, - creation_date=None, - expiration_date=None, - uses_left=None, - ) status = get_api_recovery_token_status() - if status is None: + if status is None or not status.exists: return ApiRecoveryKeyStatus( exists=False, valid=False, @@ -63,7 +50,7 @@ def get_recovery_key_status() -> ApiRecoveryKeyStatus: ) return ApiRecoveryKeyStatus( exists=True, - valid=is_recovery_token_valid(), + valid=status.valid, creation_date=status.date, expiration_date=status.expiration, uses_left=status.uses_left, diff --git a/selfprivacy_api/utils/auth.py b/selfprivacy_api/utils/auth.py index 53df508..72c791b 100644 --- a/selfprivacy_api/utils/auth.py +++ b/selfprivacy_api/utils/auth.py @@ -112,26 +112,6 @@ def create_token(name): return token -def is_recovery_token_exists(): - """Check if recovery token exists""" - with ReadUserData(UserDataFiles.TOKENS) as tokens: - return "recovery_token" in tokens - - -def is_recovery_token_valid(): - """Check if recovery token is valid""" - with ReadUserData(UserDataFiles.TOKENS) as tokens: - if "recovery_token" not in tokens: - return False - recovery_token = tokens["recovery_token"] - if "uses_left" in recovery_token and recovery_token["uses_left"] is not None: - if recovery_token["uses_left"] <= 0: - return False - if "expiration" not in recovery_token or recovery_token["expiration"] is None: - return True - return datetime.now() < parse_date(recovery_token["expiration"]) - - def _get_recovery_token(): """Get recovery token""" with ReadUserData(UserDataFiles.TOKENS) as tokens: From b3d6251d11e02d1e5f091f69e6a2c2e1432681fe Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 26 Dec 2022 14:54:07 +0000 Subject: [PATCH 25/34] refactor(tokens-repo): delete generare recovery key from auth utils --- selfprivacy_api/utils/auth.py | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/selfprivacy_api/utils/auth.py b/selfprivacy_api/utils/auth.py index 72c791b..6cdaf09 100644 --- a/selfprivacy_api/utils/auth.py +++ b/selfprivacy_api/utils/auth.py @@ -120,37 +120,6 @@ def _get_recovery_token(): return tokens["recovery_token"]["token"] -def generate_recovery_token( - expiration: typing.Optional[datetime], uses_left: typing.Optional[int] -) -> str: - """Generate a 24 bytes recovery token and return a mneomnic word list. - Write a string representation of the recovery token to the tokens.json file. - """ - # expires must be a date or None - # uses_left must be an integer or None - if expiration is not None: - if not isinstance(expiration, datetime): - raise TypeError("expires must be a datetime object") - if uses_left is not None: - if not isinstance(uses_left, int): - raise TypeError("uses_left must be an integer") - if uses_left <= 0: - raise ValueError("uses_left must be greater than 0") - - recovery_token = secrets.token_bytes(24) - recovery_token_str = recovery_token.hex() - with WriteUserData(UserDataFiles.TOKENS) as tokens: - tokens["recovery_token"] = { - "token": recovery_token_str, - "date": str(datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f")), - "expiration": expiration.strftime("%Y-%m-%dT%H:%M:%S.%f") - if expiration is not None - else None, - "uses_left": uses_left if uses_left is not None else None, - } - return Mnemonic(language="english").to_mnemonic(recovery_token) - - def _get_new_device_auth_token(): """Get new device auth token. If it is expired, return None""" with ReadUserData(UserDataFiles.TOKENS) as tokens: From 25326b75ca9be6cfc119197ed7db29bdc29bee90 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 26 Dec 2022 14:55:40 +0000 Subject: [PATCH 26/34] refactor(tokens-repo): delete create token from auth utils --- selfprivacy_api/utils/auth.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/selfprivacy_api/utils/auth.py b/selfprivacy_api/utils/auth.py index 6cdaf09..860fa0d 100644 --- a/selfprivacy_api/utils/auth.py +++ b/selfprivacy_api/utils/auth.py @@ -97,21 +97,6 @@ def _generate_token(): return token -def create_token(name): - """Create new token""" - token = _generate_token() - name = _validate_token_name(name) - with WriteUserData(UserDataFiles.TOKENS) as tokens: - tokens["tokens"].append( - { - "token": token, - "name": name, - "date": str(datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f")), - } - ) - return token - - def _get_recovery_token(): """Get recovery token""" with ReadUserData(UserDataFiles.TOKENS) as tokens: From 87ea88c50ac57a8fc1eb854db05a647ade493227 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 26 Dec 2022 14:56:39 +0000 Subject: [PATCH 27/34] refactor(tokens-repo): delete get token name from auth utils --- selfprivacy_api/utils/auth.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/selfprivacy_api/utils/auth.py b/selfprivacy_api/utils/auth.py index 860fa0d..2995aea 100644 --- a/selfprivacy_api/utils/auth.py +++ b/selfprivacy_api/utils/auth.py @@ -73,15 +73,6 @@ def is_token_valid(token): return False -def get_token_name(token: str) -> typing.Optional[str]: - """Return the name of the token provided""" - with ReadUserData(UserDataFiles.TOKENS) as tokens: - for t in tokens["tokens"]: - if t["token"] == token: - return t["name"] - return None - - class BasicTokenInfo(BaseModel): """Token info""" From 7d9bccf4ec67b83a017caa19495092b1ad33c176 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 26 Dec 2022 15:18:02 +0000 Subject: [PATCH 28/34] fix(tokens-repo): make is_token_valid infallible --- .../repositories/tokens/abstract_tokens_repository.py | 5 +---- .../test_graphql/test_repository/test_tokens_repository.py | 7 +++++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py index 7601ef4..dcd2b0d 100644 --- a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py @@ -63,10 +63,7 @@ class AbstractTokensRepository(ABC): def is_token_valid(self, token_string: str) -> bool: """Check if the token is valid""" - token = self.get_token_by_token_string(token_string) - if token is None: - return False - return True + return token_string in [token.token for token in self.get_tokens()] def is_token_name_exists(self, token_name: str) -> bool: """Check if the token name exists""" diff --git a/tests/test_graphql/test_repository/test_tokens_repository.py b/tests/test_graphql/test_repository/test_tokens_repository.py index a95878d..dd4f0ef 100644 --- a/tests/test_graphql/test_repository/test_tokens_repository.py +++ b/tests/test_graphql/test_repository/test_tokens_repository.py @@ -207,6 +207,13 @@ def test_get_token_by_non_existent_name(some_tokens_repo): assert repo.get_token_by_name(token_name="badname") is None +def test_is_token_valid(some_tokens_repo): + repo = some_tokens_repo + token = repo.get_tokens()[0] + assert repo.is_token_valid(token.token) + assert not repo.is_token_valid("gibberish") + + def test_is_token_name_pair_valid(some_tokens_repo): repo = some_tokens_repo token = repo.get_tokens()[0] From 8235c3595c63f611903c91be0d0f8cab34c07d0a Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 26 Dec 2022 15:20:58 +0000 Subject: [PATCH 29/34] refactor(tokens-repo): delete is_token_valid from auth utils --- selfprivacy_api/actions/api_tokens.py | 5 +++++ selfprivacy_api/dependencies.py | 2 +- selfprivacy_api/graphql/__init__.py | 2 +- selfprivacy_api/utils/auth.py | 7 ------- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/selfprivacy_api/actions/api_tokens.py b/selfprivacy_api/actions/api_tokens.py index 3b180e8..57828bc 100644 --- a/selfprivacy_api/actions/api_tokens.py +++ b/selfprivacy_api/actions/api_tokens.py @@ -39,6 +39,11 @@ def get_api_tokens_with_caller_flag(caller_token: str) -> list[TokenInfoWithIsCa ] +def is_token_valid(token) -> bool: + """Check if token is valid""" + return TOKEN_REPO.is_token_valid(token) + + class NotFoundException(Exception): """Not found exception""" diff --git a/selfprivacy_api/dependencies.py b/selfprivacy_api/dependencies.py index 9568a40..1348f65 100644 --- a/selfprivacy_api/dependencies.py +++ b/selfprivacy_api/dependencies.py @@ -2,7 +2,7 @@ from fastapi import Depends, HTTPException, status from fastapi.security import APIKeyHeader from pydantic import BaseModel -from selfprivacy_api.utils.auth import is_token_valid +from selfprivacy_api.actions.api_tokens import is_token_valid class TokenHeader(BaseModel): diff --git a/selfprivacy_api/graphql/__init__.py b/selfprivacy_api/graphql/__init__.py index 7372197..6124a1a 100644 --- a/selfprivacy_api/graphql/__init__.py +++ b/selfprivacy_api/graphql/__init__.py @@ -4,7 +4,7 @@ import typing from strawberry.permission import BasePermission from strawberry.types import Info -from selfprivacy_api.utils.auth import is_token_valid +from selfprivacy_api.actions.api_tokens import is_token_valid class IsAuthenticated(BasePermission): diff --git a/selfprivacy_api/utils/auth.py b/selfprivacy_api/utils/auth.py index 2995aea..48cf450 100644 --- a/selfprivacy_api/utils/auth.py +++ b/selfprivacy_api/utils/auth.py @@ -66,13 +66,6 @@ def _validate_token_name(name): return name -def is_token_valid(token): - """Check if token is valid""" - if token in _get_tokens(): - return True - return False - - class BasicTokenInfo(BaseModel): """Token info""" From 450ff41ebddff1000c3970347fc0cacac55841be Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 26 Dec 2022 15:51:12 +0000 Subject: [PATCH 30/34] feat(tokens-repo): make device names unique before storage --- .../tokens/abstract_tokens_repository.py | 18 +++++++++++++++++- .../test_repository/test_tokens_repository.py | 11 +++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py index dcd2b0d..931f64d 100644 --- a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py @@ -2,6 +2,8 @@ from abc import ABC, abstractmethod from datetime import datetime from typing import Optional from mnemonic import Mnemonic +from secrets import randbelow +import re from selfprivacy_api.models.tokens.token import Token from selfprivacy_api.repositories.tokens.exceptions import ( @@ -39,7 +41,8 @@ class AbstractTokensRepository(ABC): def create_token(self, device_name: str) -> Token: """Create new token""" - new_token = Token.generate(device_name) + unique_name = self._make_unique_device_name(device_name) + new_token = Token.generate(unique_name) self._store_token(new_token) @@ -160,6 +163,19 @@ class AbstractTokensRepository(ABC): def _get_stored_new_device_key(self) -> Optional[NewDeviceKey]: """Retrieves new device key that is already stored.""" + def _make_unique_device_name(self, name: str) -> str: + """Token name must be an alphanumeric string and not empty. + Replace invalid characters with '_' + If name exists, add a random number to the end of the name until it is unique. + """ + if not re.match("^[a-zA-Z0-9]*$", name): + name = re.sub("[^a-zA-Z0-9]", "_", name) + if name == "": + name = "Unknown device" + while self.is_token_name_exists(name): + name += str(randbelow(10)) + return name + # TODO: find a proper place for it def _assert_mnemonic(self, hex_key: str, mnemonic_phrase: str): """Return true if hex string matches the phrase, false otherwise diff --git a/tests/test_graphql/test_repository/test_tokens_repository.py b/tests/test_graphql/test_repository/test_tokens_repository.py index dd4f0ef..b9a9277 100644 --- a/tests/test_graphql/test_repository/test_tokens_repository.py +++ b/tests/test_graphql/test_repository/test_tokens_repository.py @@ -257,6 +257,17 @@ def test_create_token(empty_repo, mock_token_generate): ] +def test_create_token_existing(some_tokens_repo): + repo = some_tokens_repo + old_token = repo.get_tokens()[0] + + new_token = repo.create_token(device_name=old_token.device_name) + assert new_token.device_name != old_token.device_name + + assert old_token in repo.get_tokens() + assert new_token in repo.get_tokens() + + def test_delete_token(some_tokens_repo): repo = some_tokens_repo original_tokens = repo.get_tokens() From 3ecfb2eacbb157e2c5a22e30913df3778e3eecfd Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 26 Dec 2022 15:54:33 +0000 Subject: [PATCH 31/34] refactor(tokens-repo): delete auth utils --- selfprivacy_api/utils/auth.py | 103 ---------------------------------- 1 file changed, 103 deletions(-) delete mode 100644 selfprivacy_api/utils/auth.py diff --git a/selfprivacy_api/utils/auth.py b/selfprivacy_api/utils/auth.py deleted file mode 100644 index 48cf450..0000000 --- a/selfprivacy_api/utils/auth.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 -"""Token management utils""" -import secrets -from datetime import datetime, timedelta -import re -import typing - -from pydantic import BaseModel -from mnemonic import Mnemonic - -from . import ReadUserData, UserDataFiles, WriteUserData, parse_date - -""" -Token are stored in the tokens.json file. -File contains device tokens, recovery token and new device auth token. -File structure: -{ - "tokens": [ - { - "token": "device token", - "name": "device name", - "date": "date of creation", - } - ], - "recovery_token": { - "token": "recovery token", - "date": "date of creation", - "expiration": "date of expiration", - "uses_left": "number of uses left" - }, - "new_device": { - "token": "new device auth token", - "date": "date of creation", - "expiration": "date of expiration", - } -} -Recovery token may or may not have expiration date and uses_left. -There may be no recovery token at all. -Device tokens must be unique. -""" - - -def _get_tokens(): - """Get all tokens as list of tokens of every device""" - with ReadUserData(UserDataFiles.TOKENS) as tokens: - return [token["token"] for token in tokens["tokens"]] - - -def _get_token_names(): - """Get all token names""" - with ReadUserData(UserDataFiles.TOKENS) as tokens: - return [t["name"] for t in tokens["tokens"]] - - -def _validate_token_name(name): - """Token name must be an alphanumeric string and not empty. - Replace invalid characters with '_' - If token name exists, add a random number to the end of the name until it is unique. - """ - if not re.match("^[a-zA-Z0-9]*$", name): - name = re.sub("[^a-zA-Z0-9]", "_", name) - if name == "": - name = "Unknown device" - while name in _get_token_names(): - name += str(secrets.randbelow(10)) - return name - - -class BasicTokenInfo(BaseModel): - """Token info""" - - name: str - date: datetime - - -def _generate_token(): - """Generates new token and makes sure it is unique""" - token = secrets.token_urlsafe(32) - while token in _get_tokens(): - token = secrets.token_urlsafe(32) - return token - - -def _get_recovery_token(): - """Get recovery token""" - with ReadUserData(UserDataFiles.TOKENS) as tokens: - if "recovery_token" not in tokens: - return None - return tokens["recovery_token"]["token"] - - -def _get_new_device_auth_token(): - """Get new device auth token. If it is expired, return None""" - with ReadUserData(UserDataFiles.TOKENS) as tokens: - if "new_device" not in tokens: - return None - new_device = tokens["new_device"] - if "expiration" not in new_device: - return None - expiration = parse_date(new_device["expiration"]) - if datetime.now() > expiration: - return None - return new_device["token"] From 67872d7c556314b2a58bdbb8daa028746ebbf3d5 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 26 Dec 2022 16:22:16 +0000 Subject: [PATCH 32/34] test(tokens-repo): use mnemonic_from_hex consistently --- .../test_repository/test_tokens_repository.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/tests/test_graphql/test_repository/test_tokens_repository.py b/tests/test_graphql/test_repository/test_tokens_repository.py index b9a9277..020a868 100644 --- a/tests/test_graphql/test_repository/test_tokens_repository.py +++ b/tests/test_graphql/test_repository/test_tokens_repository.py @@ -33,6 +33,10 @@ ORIGINAL_DEVICE_NAMES = [ ] +def mnemonic_from_hex(hexkey): + return Mnemonic(language="english").to_mnemonic(bytes.fromhex(hexkey)) + + @pytest.fixture def empty_keys(mocker, datadir): mocker.patch("selfprivacy_api.utils.TOKENS_FILE", new=datadir / "empty_keys.json") @@ -388,9 +392,7 @@ def test_use_mnemonic_expired_recovery_key( with pytest.raises(RecoveryKeyNotFound): token = repo.use_mnemonic_recovery_key( - mnemonic_phrase=Mnemonic(language="english").to_mnemonic( - bytes.fromhex(recovery_key.key) - ), + mnemonic_phrase=mnemonic_from_hex(recovery_key.key), device_name="newdevice", ) @@ -455,9 +457,7 @@ def test_use_mnemonic_recovery_key(some_tokens_repo, recovery_key_uses_left): recovery_key = repo.get_recovery_key() token = repo.use_mnemonic_recovery_key( - mnemonic_phrase=Mnemonic(language="english").to_mnemonic( - bytes.fromhex(recovery_key.key) - ), + mnemonic_phrase=mnemonic_from_hex(recovery_key.key), device_name="newdevice", ) @@ -531,10 +531,6 @@ def test_use_not_exists_mnemonic_new_device_key( ) -def mnemonic_from_hex(hexkey): - return Mnemonic(language="english").to_mnemonic(bytes.fromhex(hexkey)) - - def test_use_mnemonic_new_device_key(empty_repo): repo = empty_repo key = repo.get_new_device_key() From 999dd95cab5d4d6b23a812b74378b2bd02aad873 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Tue, 27 Dec 2022 07:10:14 +0300 Subject: [PATCH 33/34] ci: Add redis to CI pipeline --- .drone.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.drone.yml b/.drone.yml index 0f5f93a..24ab5da 100644 --- a/.drone.yml +++ b/.drone.yml @@ -5,12 +5,16 @@ name: default steps: - name: Run Tests and Generate Coverage Report commands: + - kill $(ps aux | grep '[r]edis-server 127.0.0.1:6389' | awk '{print $2}') + - redis-server --bind 127.0.0.1 --port 6389 >/dev/null & - coverage run -m pytest -q - coverage xml - sonar-scanner -Dsonar.projectKey=SelfPrivacy-REST-API -Dsonar.sources=. -Dsonar.host.url=http://analyzer.lan:9000 -Dsonar.login="$SONARQUBE_TOKEN" environment: SONARQUBE_TOKEN: from_secret: SONARQUBE_TOKEN + USE_REDIS_PORT: 6389 + - name: Run Bandit Checks commands: From cb403a94bdcfa9b853273d297e2b4d3ffac23402 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 30 Dec 2022 21:06:16 +0300 Subject: [PATCH 34/34] fix: typing --- selfprivacy_api/actions/api_tokens.py | 2 +- selfprivacy_api/jobs/__init__.py | 49 ++++++++++--------- .../tokens/abstract_tokens_repository.py | 11 +++-- .../tokens/json_tokens_repository.py | 8 +-- .../tokens/redis_tokens_repository.py | 46 ++++++++++------- 5 files changed, 67 insertions(+), 49 deletions(-) diff --git a/selfprivacy_api/actions/api_tokens.py b/selfprivacy_api/actions/api_tokens.py index 57828bc..38133fd 100644 --- a/selfprivacy_api/actions/api_tokens.py +++ b/selfprivacy_api/actions/api_tokens.py @@ -150,7 +150,7 @@ def get_new_device_auth_token() -> str: return Mnemonic(language="english").to_mnemonic(bytes.fromhex(key.key)) -def use_new_device_auth_token(mnemonic_phrase, name) -> str: +def use_new_device_auth_token(mnemonic_phrase, name) -> Optional[str]: """Use the new device auth token by converting the mnemonic string to a byte array. If the mnemonic phrase is valid then generate a device token and return it. New device auth token must be deleted. diff --git a/selfprivacy_api/jobs/__init__.py b/selfprivacy_api/jobs/__init__.py index 1547b84..fe4a053 100644 --- a/selfprivacy_api/jobs/__init__.py +++ b/selfprivacy_api/jobs/__init__.py @@ -97,8 +97,8 @@ class Jobs: error=None, result=None, ) - r = RedisPool().get_connection() - _store_job_as_hash(r, _redis_key_from_uuid(job.uid), job) + redis = RedisPool().get_connection() + _store_job_as_hash(redis, _redis_key_from_uuid(job.uid), job) return job @staticmethod @@ -113,10 +113,10 @@ class Jobs: """ Remove a job from the jobs list. """ - r = RedisPool().get_connection() + redis = RedisPool().get_connection() key = _redis_key_from_uuid(job_uuid) - if (r.exists(key)): - r.delete(key) + if redis.exists(key): + redis.delete(key) return True return False @@ -149,12 +149,12 @@ class Jobs: if status in (JobStatus.FINISHED, JobStatus.ERROR): job.finished_at = datetime.datetime.now() - r = RedisPool().get_connection() + redis = RedisPool().get_connection() key = _redis_key_from_uuid(job.uid) - if r.exists(key): - _store_job_as_hash(r, key, job) + if redis.exists(key): + _store_job_as_hash(redis, key, job) if status in (JobStatus.FINISHED, JobStatus.ERROR): - r.expire(key, JOB_EXPIRATION_SECONDS) + redis.expire(key, JOB_EXPIRATION_SECONDS) return job @@ -163,10 +163,10 @@ class Jobs: """ Get a job from the jobs list. """ - r = RedisPool().get_connection() + redis = RedisPool().get_connection() key = _redis_key_from_uuid(uid) - if r.exists(key): - return _job_from_hash(r, key) + if redis.exists(key): + return _job_from_hash(redis, key) return None @staticmethod @@ -174,9 +174,14 @@ class Jobs: """ Get the jobs list. """ - r = RedisPool().get_connection() - jobs = r.keys("jobs:*") - return [_job_from_hash(r, job_key) for job_key in jobs] + redis = RedisPool().get_connection() + job_keys = redis.keys("jobs:*") + jobs = [] + for job_key in job_keys: + job = _job_from_hash(redis, job_key) + if job is not None: + jobs.append(job) + return jobs @staticmethod def is_busy() -> bool: @@ -189,11 +194,11 @@ class Jobs: return False -def _redis_key_from_uuid(uuid): - return "jobs:" + str(uuid) +def _redis_key_from_uuid(uuid_string): + return "jobs:" + str(uuid_string) -def _store_job_as_hash(r, redis_key, model): +def _store_job_as_hash(redis, redis_key, model): for key, value in model.dict().items(): if isinstance(value, uuid.UUID): value = str(value) @@ -201,12 +206,12 @@ def _store_job_as_hash(r, redis_key, model): value = value.isoformat() if isinstance(value, JobStatus): value = value.value - r.hset(redis_key, key, str(value)) + redis.hset(redis_key, key, str(value)) -def _job_from_hash(r, redis_key): - if r.exists(redis_key): - job_dict = r.hgetall(redis_key) +def _job_from_hash(redis, redis_key): + if redis.exists(redis_key): + job_dict = redis.hgetall(redis_key) for date in [ "created_at", "updated_at", diff --git a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py index 931f64d..3a20ede 100644 --- a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py @@ -17,7 +17,7 @@ from selfprivacy_api.models.tokens.new_device_key import NewDeviceKey class AbstractTokensRepository(ABC): - def get_token_by_token_string(self, token_string: str) -> Optional[Token]: + def get_token_by_token_string(self, token_string: str) -> Token: """Get the token by token""" tokens = self.get_tokens() for token in tokens: @@ -26,7 +26,7 @@ class AbstractTokensRepository(ABC): raise TokenNotFound("Token not found!") - def get_token_by_name(self, token_name: str) -> Optional[Token]: + def get_token_by_name(self, token_name: str) -> Token: """Get the token by name""" tokens = self.get_tokens() for token in tokens: @@ -101,7 +101,12 @@ class AbstractTokensRepository(ABC): if not self.is_recovery_key_valid(): raise RecoveryKeyNotFound("Recovery key not found") - recovery_hex_key = self.get_recovery_key().key + recovery_key = self.get_recovery_key() + + if recovery_key is None: + raise RecoveryKeyNotFound("Recovery key not found") + + recovery_hex_key = recovery_key.key if not self._assert_mnemonic(recovery_hex_key, mnemonic_phrase): raise RecoveryKeyNotFound("Recovery key not found") diff --git a/selfprivacy_api/repositories/tokens/json_tokens_repository.py b/selfprivacy_api/repositories/tokens/json_tokens_repository.py index 394c046..77e1311 100644 --- a/selfprivacy_api/repositories/tokens/json_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/json_tokens_repository.py @@ -85,13 +85,13 @@ class JsonTokensRepository(AbstractTokensRepository): recovery_key = RecoveryKey.generate(expiration, uses_left) with WriteUserData(UserDataFiles.TOKENS) as tokens_file: - expiration = recovery_key.expires_at - if expiration is not None: - expiration = expiration.strftime(DATETIME_FORMAT) + key_expiration: Optional[str] = None + if recovery_key.expires_at is not None: + key_expiration = recovery_key.expires_at.strftime(DATETIME_FORMAT) tokens_file["recovery_token"] = { "token": recovery_key.key, "date": recovery_key.created_at.strftime(DATETIME_FORMAT), - "expiration": expiration, + "expiration": key_expiration, "uses_left": recovery_key.uses_left, } diff --git a/selfprivacy_api/repositories/tokens/redis_tokens_repository.py b/selfprivacy_api/repositories/tokens/redis_tokens_repository.py index d665553..c72e231 100644 --- a/selfprivacy_api/repositories/tokens/redis_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/redis_tokens_repository.py @@ -32,29 +32,34 @@ class RedisTokensRepository(AbstractTokensRepository): def get_tokens(self) -> list[Token]: """Get the tokens""" - r = self.connection - token_keys = r.keys(TOKENS_PREFIX + "*") - return [self._token_from_hash(key) for key in token_keys] + redis = self.connection + token_keys = redis.keys(TOKENS_PREFIX + "*") + tokens = [] + for key in token_keys: + token = self._token_from_hash(key) + if token is not None: + tokens.append(token) + return tokens def delete_token(self, input_token: Token) -> None: """Delete the token""" - r = self.connection + redis = self.connection key = RedisTokensRepository._token_redis_key(input_token) if input_token not in self.get_tokens(): raise TokenNotFound - r.delete(key) + redis.delete(key) def reset(self): for token in self.get_tokens(): self.delete_token(token) self.delete_new_device_key() - r = self.connection - r.delete(RECOVERY_KEY_REDIS_KEY) + redis = self.connection + redis.delete(RECOVERY_KEY_REDIS_KEY) def get_recovery_key(self) -> Optional[RecoveryKey]: """Get the recovery key""" - r = self.connection - if r.exists(RECOVERY_KEY_REDIS_KEY): + redis = self.connection + if redis.exists(RECOVERY_KEY_REDIS_KEY): return self._recovery_key_from_hash(RECOVERY_KEY_REDIS_KEY) return None @@ -74,8 +79,8 @@ class RedisTokensRepository(AbstractTokensRepository): def delete_new_device_key(self) -> None: """Delete the new device key""" - r = self.connection - r.delete(NEW_DEVICE_KEY_REDIS_KEY) + redis = self.connection + redis.delete(NEW_DEVICE_KEY_REDIS_KEY) @staticmethod def _token_redis_key(token: Token) -> str: @@ -89,10 +94,13 @@ class RedisTokensRepository(AbstractTokensRepository): def _decrement_recovery_token(self): """Decrement recovery key use count by one""" if self.is_recovery_key_valid(): - uses_left = self.get_recovery_key().uses_left + recovery_key = self.get_recovery_key() + if recovery_key is None: + return + uses_left = recovery_key.uses_left if uses_left is not None: - r = self.connection - r.hset(RECOVERY_KEY_REDIS_KEY, "uses_left", uses_left - 1) + redis = self.connection + redis.hset(RECOVERY_KEY_REDIS_KEY, "uses_left", uses_left - 1) def _get_stored_new_device_key(self) -> Optional[NewDeviceKey]: """Retrieves new device key that is already stored.""" @@ -116,9 +124,9 @@ class RedisTokensRepository(AbstractTokensRepository): d[key] = None def _model_dict_from_hash(self, redis_key: str) -> Optional[dict]: - r = self.connection - if r.exists(redis_key): - token_dict = r.hgetall(redis_key) + redis = self.connection + if redis.exists(redis_key): + token_dict = redis.hgetall(redis_key) RedisTokensRepository._prepare_model_dict(token_dict) return token_dict return None @@ -139,8 +147,8 @@ class RedisTokensRepository(AbstractTokensRepository): return self._hash_as_model(redis_key, NewDeviceKey) def _store_model_as_hash(self, redis_key, model): - r = self.connection + redis = self.connection for key, value in model.dict().items(): if isinstance(value, datetime): value = value.isoformat() - r.hset(redis_key, key, str(value)) + redis.hset(redis_key, key, str(value))