diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 79bf180a..5e6fa749 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -12,8 +12,8 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [3.11] - django-version: [Django~=4.2] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + django-version: ["django~=4.2", "django~=5.1", "django~=5.2"] steps: - uses: actions/checkout@v2 @@ -24,8 +24,8 @@ jobs: - name: Install Dependencies run: | pip install ${{ matrix.django-version }} - python setup.py install pip install -r test_requirements.txt + pip install . - name: Run Tests run: | python runtests.py diff --git a/gdpr/anonymizers/base.py b/gdpr/anonymizers/base.py index 7620e605..42a17c80 100644 --- a/gdpr/anonymizers/base.py +++ b/gdpr/anonymizers/base.py @@ -138,9 +138,14 @@ def __init__(self, max_anonymization_range: int = None, ignore_empty_values: boo self.max_anonymization_range = max_anonymization_range super().__init__(ignore_empty_values, empty_values) - def get_numeric_encryption_key(self, encryption_key: str, value: Union[int, float] = None) -> int: + def get_numeric_encryption_key( + self, + encryption_key: str, + value: Union[int, float] = None, + allow_zero: bool = True + ) -> int: """ - From `encryption_key` create it's numeric counterpart of appropriate length. + From `encryption_key` create its numeric counterpart of appropriate length. If value is supplied then the appropriate length is based on it if not the parameter `self.max_anonymization_range` is used. @@ -151,11 +156,18 @@ def get_numeric_encryption_key(self, encryption_key: str, value: Union[int, floa :param encryption_key: The encryption key generated by anonymizer. :param value: Value to which the result of this function will be used. + :param allow_zero: VWhether returning zero is permitted. :return: Numeric counterpart of encryption_key """ if value is None: if self.max_anonymization_range is None: - return numerize_key(encryption_key) - return numerize_key(encryption_key) % self.max_anonymization_range - - return numerize_key(encryption_key) % 10 ** get_number_guess_len(value) + numeric_key = numerize_key(encryption_key) + else: + numeric_key = numerize_key(encryption_key) % self.max_anonymization_range + else: + numeric_key = numerize_key(encryption_key) % (10 ** get_number_guess_len(value)) + + if numeric_key == 0 and not allow_zero: + return 1 + else: + return numeric_key diff --git a/gdpr/anonymizers/gis.py b/gdpr/anonymizers/gis.py index 999ccec2..afe5453c 100644 --- a/gdpr/anonymizers/gis.py +++ b/gdpr/anonymizers/gis.py @@ -1,4 +1,5 @@ """Since django 1.11 djnago-GIS requires GDAL.""" + import logging from typing import Optional @@ -50,18 +51,26 @@ def get_encrypted_value(self, value, encryption_key: str): from django.contrib.gis.geos import Point new_val: Point = Point(value.tuple) - new_val.x = (new_val.x + self.get_numeric_encryption_key(encryption_key, int(new_val.x))) % self.max_x_range - new_val.y = (new_val.y + self.get_numeric_encryption_key(encryption_key, int(new_val.y))) % self.max_y_range + new_val.x = ( + new_val.x + self.get_numeric_encryption_key(encryption_key, int(new_val.x), allow_zero=False) + ) % self.max_x_range + new_val.y = ( + new_val.y + self.get_numeric_encryption_key(encryption_key, int(new_val.y), allow_zero=False) + ) % self.max_y_range return new_val def get_decrypted_value(self, value, encryption_key: str): if not is_gis_installed(): - raise ImproperlyConfigured('Unable to load django GIS.') + raise ImproperlyConfigured("Unable to load django GIS.") from django.contrib.gis.geos import Point new_val: Point = Point(value.tuple) - new_val.x = (new_val.x - self.get_numeric_encryption_key(encryption_key, int(new_val.x))) % self.max_x_range - new_val.y = (new_val.y - self.get_numeric_encryption_key(encryption_key, int(new_val.y))) % self.max_y_range + new_val.x = ( + new_val.x - self.get_numeric_encryption_key(encryption_key, int(new_val.x), allow_zero=False) + ) % self.max_x_range + new_val.y = ( + new_val.y - self.get_numeric_encryption_key(encryption_key, int(new_val.y), allow_zero=False) + ) % self.max_y_range return new_val diff --git a/gdpr/anonymizers/local/cs.py b/gdpr/anonymizers/local/cs.py index fa207d1e..2f25f92e 100644 --- a/gdpr/anonymizers/local/cs.py +++ b/gdpr/anonymizers/local/cs.py @@ -345,7 +345,7 @@ def get_encrypted_value(self, value, encryption_key: str): account = CzechAccountNumber.parse(value) if self.use_smart_method and account.check_account_format(): - return str(account.brute_force_next(self.get_numeric_encryption_key(encryption_key))) + return str(account.brute_force_next(self.get_numeric_encryption_key(encryption_key, allow_zero=False))) account.num = int(encrypt_text(encryption_key, str(account.num), NUMBERS)) @@ -355,7 +355,7 @@ def get_decrypted_value(self, value: Any, encryption_key: str): account = CzechAccountNumber.parse(value) if self.use_smart_method and account.check_account_format(): - return str(account.brute_force_prev(self.get_numeric_encryption_key(encryption_key))) + return str(account.brute_force_prev(self.get_numeric_encryption_key(encryption_key, allow_zero=False))) account.num = int(decrypt_text(encryption_key, str(account.num), NUMBERS)) @@ -369,13 +369,13 @@ def get_encrypted_value(self, value: Any, encryption_key: str): iban = CzechIBAN.parse(value) if not iban.check_iban_format(): raise ValidationError(f'IBAN \'{value}\' does not appear to be valid czech IBAN.') - return str(iban.brute_force_next(self.get_numeric_encryption_key(encryption_key))) + return str(iban.brute_force_next(self.get_numeric_encryption_key(encryption_key, allow_zero=False))) def get_decrypted_value(self, value: Any, encryption_key: str): iban = CzechIBAN.parse(value) if not iban.check_iban_format(): raise ValidationError(f'IBAN \'{value}\' does not appear to be valid czech IBAN.') - return str(iban.brute_force_prev(self.get_numeric_encryption_key(encryption_key))) + return str(iban.brute_force_prev(self.get_numeric_encryption_key(encryption_key, allow_zero=False))) class CzechPhoneNumberFieldAnonymizer(FieldAnonymizer): diff --git a/gdpr/migrations/0002_auto_20180509_1518.py b/gdpr/migrations/0002_auto_20180509_1518.py index 7457d27f..31560bf9 100644 --- a/gdpr/migrations/0002_auto_20180509_1518.py +++ b/gdpr/migrations/0002_auto_20180509_1518.py @@ -5,7 +5,6 @@ import datetime from django.db import migrations, models -from django.utils.timezone import utc class Migration(migrations.Migration): @@ -22,8 +21,10 @@ class Migration(migrations.Migration): migrations.AddField( model_name='legalreason', name='issued_at', - field=models.DateTimeField(default=datetime.datetime(2018, 5, 9, 13, 18, 7, 317147, tzinfo=utc), - verbose_name='issued at'), + field=models.DateTimeField( + default=datetime.datetime(2018, 5, 9, 13, 18, 7, 317147, tzinfo=datetime.timezone.utc), + verbose_name='issued at' + ), preserve_default=False, ), migrations.AlterField( diff --git a/gdpr/tests/test_cs_fields.py b/gdpr/tests/test_cs_fields.py index 036adeed..7c054089 100644 --- a/gdpr/tests/test_cs_fields.py +++ b/gdpr/tests/test_cs_fields.py @@ -13,6 +13,7 @@ class TestCzechAccountNumberField(TestCase): def setUpTestData(cls): cls.field = CzechAccountNumberFieldAnonymizer() cls.encryption_key = 'LoremIpsumDolorSitAmet' + cls.zero_remainder_encryption_key = 'Vu' def test_account_number_simple_field(self): account_number = '2501277007/2010' @@ -35,6 +36,17 @@ def test_account_number_simple_field_smart_method(self): assert_equal(out_decrypt, account_number) + def test_account_number_simple_field_smart_method_using_encryption_key_having_zero_modulo_remainder(self): + field = CzechAccountNumberFieldAnonymizer(use_smart_method=True) + account_number = '2501277007/2010' + out = field.get_encrypted_value(account_number, self.zero_remainder_encryption_key) + + assert_not_equal(out, account_number) + + out_decrypt = field.get_decrypted_value(out, self.zero_remainder_encryption_key) + + assert_equal(out_decrypt, account_number) + def test_account_number_with_pre_num_field(self): account_number = '19-2000145399/0800' out = self.field.get_encrypted_value(account_number, self.encryption_key) @@ -79,6 +91,7 @@ class TestCzechIBANSmartFieldAnonymizer(TestCase): def setUpTestData(cls): cls.field = CzechIBANSmartFieldAnonymizer() cls.encryption_key = 'LoremIpsumDolorSitAmet' + cls.zero_remainder_encryption_key = 'Vu' cls.text_iban = 'CZ65 0800 0000 1920 0014 5399' cls.no_space_text_iban = 'CZ6508000000192000145399' cls.invalid_text_iban = 'CZ00 0800 0000 1920 0014 5399' @@ -91,6 +104,13 @@ def test_czech_iban_field(self): out_decrypted = self.field.get_decrypted_value(out, self.encryption_key) assert_equal(out_decrypted, self.text_iban) + def test_czech_iban_field_using_encryption_key_having_zero_modulo_remainder(self): + out = self.field.get_encrypted_value(self.text_iban, self.zero_remainder_encryption_key) + assert_not_equal(out, self.text_iban) + + out_decrypted = self.field.get_decrypted_value(out, self.zero_remainder_encryption_key) + assert_equal(out_decrypted, self.text_iban) + def test_czech_iban_field_no_space(self): out = self.field.get_encrypted_value(self.no_space_text_iban, self.encryption_key) assert_not_equal(out, self.no_space_text_iban) diff --git a/setup.py b/setup.py index 7406a94a..6e78b25f 100644 --- a/setup.py +++ b/setup.py @@ -24,22 +24,24 @@ def read(fname): classifiers=[ 'Development Status :: 3 - Alpha', 'Framework :: Django', - 'Framework :: Django :: 1.10', - 'Framework :: Django :: 1.11', - 'Framework :: Django :: 2.0', - 'Framework :: Django :: 2.1', + 'Framework :: Django :: 4.2', + 'Framework :: Django :: 5.1', + 'Framework :: Django :: 5.2', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3.14', 'Programming Language :: Python :: 3 :: Only', - 'Intended Audience :: Developers', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', ], + python_requires='>=3.10', install_requires=[ - 'django>=4.2', + 'django>=4.2,<6', 'skip-django-chamber>=0.7.2', 'tqdm>=4.28.1', 'pyaes>=1.6.1', diff --git a/test_requirements.txt b/test_requirements.txt index 06f97263..42511167 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -2,7 +2,8 @@ flake8==3.8.4 mypy==0.991 python-dateutil==2.7.5 django-extensions -freezegun==0.3.12 +freezegun Faker==1.0.1 -skip-django-germanium==2.4.0 +skip-django-germanium==2.6.0 django-auditlog +setuptools