diff --git a/README.md b/README.md index fd526bd..22db942 100644 --- a/README.md +++ b/README.md @@ -472,6 +472,7 @@ Available Model Fields (with required args - optional ones in docs): * `Decimal(scale, precision)` * `UUID()` * `EnumField` - by passing `choices` to any other Field type +* `EncryptedString` - by passing `encrypt_secret` and `encrypt_backend` * `ForeignKey(to)` * `ManyToMany(to, through)` diff --git a/docs/fields/encryption.md b/docs/fields/encryption.md index e4a11c0..9cc06b0 100644 --- a/docs/fields/encryption.md +++ b/docs/fields/encryption.md @@ -55,7 +55,7 @@ Note that since this backend never decrypt the stored value it's only applicable !!!note Note that provided `encrypt_secret` is first hashed itself and used as salt, so in order to - compare to stored string you need to recreate this steps. + compare to stored string you need to recreate this steps. The `order_by` will not work as encrypted strings are compared so you cannot reliably order by. ```python class Hash(ormar.Model): @@ -101,7 +101,8 @@ Value is encrypted on way to database end decrypted on way out. Can be used on a as the returned value is parsed to corresponding python type. !!!warning - Note that in `FERNET` backend you loose `filtering` possibility altogether as part of the encrypted value is a timestamp + Note that in `FERNET` backend you loose `filter`ing possibility altogether as part of the encrypted value is a timestamp. + The same goes for `order_by` as encrypted strings are compared so you cannot reliably order by. ```python class Filter(ormar.Model): diff --git a/docs/index.md b/docs/index.md index fd526bd..22db942 100644 --- a/docs/index.md +++ b/docs/index.md @@ -472,6 +472,7 @@ Available Model Fields (with required args - optional ones in docs): * `Decimal(scale, precision)` * `UUID()` * `EnumField` - by passing `choices` to any other Field type +* `EncryptedString` - by passing `encrypt_secret` and `encrypt_backend` * `ForeignKey(to)` * `ManyToMany(to, through)` diff --git a/ormar/fields/sqlalchemy_encrypted.py b/ormar/fields/sqlalchemy_encrypted.py index 06483c6..1e6eda1 100644 --- a/ormar/fields/sqlalchemy_encrypted.py +++ b/ormar/fields/sqlalchemy_encrypted.py @@ -144,7 +144,7 @@ class EncryptedString(types.TypeDecorator): self.type_: Any = type_ def __repr__(self) -> str: # pragma: nocover - return f"TEXT()" + return "TEXT()" def load_dialect_impl(self, dialect: DefaultDialect) -> Any: return dialect.type_descriptor(types.TEXT()) diff --git a/tests/test_encrypted_columns.py b/tests/test_encrypted_columns.py index 3cfc345..1f0d7d7 100644 --- a/tests/test_encrypted_columns.py +++ b/tests/test_encrypted_columns.py @@ -77,14 +77,6 @@ class Author(ormar.Model): ) -class Filter(ormar.Model): - class Meta(BaseMeta): - tablename = "filters" - - id: int = ormar.Integer(primary_key=True) - name: str = ormar.String(max_length=100, **default_fernet) - - class Hash(ormar.Model): class Meta(BaseMeta): tablename = "hashes" @@ -97,6 +89,24 @@ class Hash(ormar.Model): ) +class Filter(ormar.Model): + class Meta(BaseMeta): + tablename = "filters" + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100, **default_fernet) + hash = ormar.ForeignKey(Hash) + + +class Report(ormar.Model): + class Meta(BaseMeta): + tablename = "reports" + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + filters = ormar.ManyToMany(Filter) + + @pytest.fixture(autouse=True, scope="module") def create_test_database(): engine = sqlalchemy.create_engine(DATABASE_URL) @@ -236,3 +246,24 @@ async def test_hash_filters_works(): with pytest.raises(NoMatch): await Filter.objects.get(name__icontains="test") + + +@pytest.mark.asyncio +async def test_related_model_fields_properly_decrypted(): + async with database: + hash1 = await Hash(name="test1").save() + report = await Report.objects.create(name="Report1") + await report.filters.create(name="test1", hash=hash1) + await report.filters.create(name="test2") + + report2 = await Report.objects.select_related("filters").get() + assert report2.filters[0].name == "test1" + assert report2.filters[1].name == "test2" + + secret = hashlib.sha256("udxc32".encode()).digest() + secret = base64.urlsafe_b64encode(secret) + hashed_test1 = hashlib.sha512(secret + "test1".encode()).hexdigest() + + report2 = await Report.objects.select_related("filters__hash").get() + assert report2.filters[0].name == "test1" + assert report2.filters[0].hash.name == hashed_test1