From 8b1fc9b64a25c073b27c91b2b9fdd7d3dfce4e38 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Wed, 22 Oct 2014 05:52:50 -0700 Subject: [PATCH 01/15] Fix handling of None, and, encode not 'bytes' --- sqlalchemy_utils/types/encrypted.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/sqlalchemy_utils/types/encrypted.py b/sqlalchemy_utils/types/encrypted.py index ae5dd3b..a81f68f 100644 --- a/sqlalchemy_utils/types/encrypted.py +++ b/sqlalchemy_utils/types/encrypted.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import base64 import six -from sqlalchemy.types import TypeDecorator, String +from sqlalchemy.types import TypeDecorator, String, Binary from sqlalchemy_utils.exceptions import ImproperlyConfigured cryptography = None @@ -74,7 +74,7 @@ class AesEngine(EncryptionDecryptionBaseEngine): value = repr(value) if isinstance(value, six.text_type): value = str(value) - value = six.b(value) + value = value.encode() value = self._pad(value) encryptor = self.cipher.encryptor() encrypted = encryptor.update(value) + encryptor.finalize() @@ -113,7 +113,7 @@ class FernetEngine(EncryptionDecryptionBaseEngine): value = repr(value) if isinstance(value, six.text_type): value = str(value) - value = six.b(value) + value = value.encode() encrypted = self.fernet.encrypt(value) return encrypted @@ -196,7 +196,7 @@ class EncryptedType(TypeDecorator): engine.dispose() """ - impl = String + impl = Binary # CHANGE! def __init__(self, type_in=None, key=None, engine=None, **kwargs): """Initialization.""" @@ -225,9 +225,11 @@ class EncryptedType(TypeDecorator): def process_bind_param(self, value, dialect): """Encrypt a value on the way in.""" - return self.engine.encrypt(value) + if value is not None: + return self.engine.encrypt(value) def process_result_value(self, value, dialect): """Decrypt value on the way out.""" - decrypted_value = self.engine.decrypt(value) - return self.underlying_type.python_type(decrypted_value) + if value is not None: + decrypted_value = self.engine.decrypt(value) + return self.underlying_type.python_type(decrypted_value) From e4656164776553437f69ff4ae409a711e83e1414 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Wed, 22 Oct 2014 05:53:12 -0700 Subject: [PATCH 02/15] Add a python_type constructor (used by EncryptedType) --- sqlalchemy_utils/types/phone_number.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sqlalchemy_utils/types/phone_number.py b/sqlalchemy_utils/types/phone_number.py index ed18792..d0f8d22 100644 --- a/sqlalchemy_utils/types/phone_number.py +++ b/sqlalchemy_utils/types/phone_number.py @@ -95,6 +95,9 @@ class PhoneNumberType(types.TypeDecorator, ScalarCoercible): STORE_FORMAT = 'e164' impl = types.Unicode(20) + def python_type(self, text): + return self._coerce(text) + def __init__(self, country_code='US', max_length=20, *args, **kwargs): # Bail if phonenumbers is not found. if phonenumbers is None: From a814493a7717f0e86bc4f2ff5fff72e9e4ea25f9 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Wed, 22 Oct 2014 06:32:27 -0700 Subject: [PATCH 03/15] Allow key to be a callable --- sqlalchemy_utils/types/encrypted.py | 38 +++++++++++------------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/sqlalchemy_utils/types/encrypted.py b/sqlalchemy_utils/types/encrypted.py index a81f68f..6b9579a 100644 --- a/sqlalchemy_utils/types/encrypted.py +++ b/sqlalchemy_utils/types/encrypted.py @@ -23,14 +23,15 @@ class EncryptionDecryptionBaseEngine(object): This class must be sub-classed in order to create new engines. """ - - def __init__(self, key): - """Initialize a base engine.""" + + def _update_key(self, key): if isinstance(key, six.string_types): key = six.b(key) - self._digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) - self._digest.update(key) - self._engine_key = self._digest.finalize() + digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) + digest.update(key) + engine_key = digest.finalize() + + self._initialize_engine(engine_key) def encrypt(self, value): raise NotImplementedError('Subclasses must implement this!') @@ -45,14 +46,6 @@ class AesEngine(EncryptionDecryptionBaseEngine): BLOCK_SIZE = 16 PADDING = six.b('*') - def __init__(self, key): - super(AesEngine, self).__init__(key) - self._initialize_engine(self._engine_key) - - def _update_key(self, new_key): - parent = EncryptionDecryptionBaseEngine(new_key) - self._initialize_engine(parent._engine_key) - def _initialize_engine(self, parent_class_key): self.secret_key = parent_class_key self.iv = self.secret_key[:16] @@ -96,14 +89,6 @@ class AesEngine(EncryptionDecryptionBaseEngine): class FernetEngine(EncryptionDecryptionBaseEngine): """Provide Fernet encryption and decryption methods.""" - def __init__(self, key): - super(FernetEngine, self).__init__(key) - self._initialize_engine(self._engine_key) - - def _update_key(self, new_key): - parent = EncryptionDecryptionBaseEngine(new_key) - self._initialize_engine(parent._engine_key) - def _initialize_engine(self, parent_class_key): self.secret_key = base64.urlsafe_b64encode(parent_class_key) self.fernet = Fernet(self.secret_key) @@ -212,7 +197,7 @@ class EncryptedType(TypeDecorator): self._key = key if not engine: engine = AesEngine - self.engine = engine(self._key) + self.engine = engine() @property def key(self): @@ -221,15 +206,20 @@ class EncryptedType(TypeDecorator): @key.setter def key(self, value): self._key = value - self.engine._update_key(self._key) + + def _update_key(self): + key = self._key() if callable(self._key) else self._key + self.engine._update_key(key) def process_bind_param(self, value, dialect): """Encrypt a value on the way in.""" if value is not None: + self._update_key() return self.engine.encrypt(value) def process_result_value(self, value, dialect): """Decrypt value on the way out.""" if value is not None: + self._update_key() decrypted_value = self.engine.decrypt(value) return self.underlying_type.python_type(decrypted_value) From 4dcc11c3db1f6e26b732dfa0769297149e528d2b Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Wed, 29 Oct 2014 19:08:59 -0700 Subject: [PATCH 04/15] Add support for 'python_type' to 'ColorType' --- sqlalchemy_utils/types/color.py | 1 + sqlalchemy_utils/types/encrypted.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sqlalchemy_utils/types/color.py b/sqlalchemy_utils/types/color.py index 1d4624b..0064243 100644 --- a/sqlalchemy_utils/types/color.py +++ b/sqlalchemy_utils/types/color.py @@ -49,6 +49,7 @@ class ColorType(types.TypeDecorator, ScalarCoercible): """ STORE_FORMAT = u'hex' impl = types.Unicode(20) + python_type = colour.Color def __init__(self, max_length=20, *args, **kwargs): # Fail if colour is not found. diff --git a/sqlalchemy_utils/types/encrypted.py b/sqlalchemy_utils/types/encrypted.py index 6b9579a..aa1506f 100644 --- a/sqlalchemy_utils/types/encrypted.py +++ b/sqlalchemy_utils/types/encrypted.py @@ -23,7 +23,7 @@ class EncryptionDecryptionBaseEngine(object): This class must be sub-classed in order to create new engines. """ - + def _update_key(self, key): if isinstance(key, six.string_types): key = six.b(key) @@ -208,7 +208,7 @@ class EncryptedType(TypeDecorator): self._key = value def _update_key(self): - key = self._key() if callable(self._key) else self._key + key = self._key() if callable(self._key) else self._key self.engine._update_key(key) def process_bind_param(self, value, dialect): From ce4a9bba1855eb8c49054e95c6ed698c63f96d91 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Thu, 30 Oct 2014 13:38:10 -0700 Subject: [PATCH 05/15] Add support for 'coercion_listener' to the encrypted type. --- sqlalchemy_utils/types/encrypted.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sqlalchemy_utils/types/encrypted.py b/sqlalchemy_utils/types/encrypted.py index aa1506f..e0b0957 100644 --- a/sqlalchemy_utils/types/encrypted.py +++ b/sqlalchemy_utils/types/encrypted.py @@ -3,6 +3,7 @@ import base64 import six from sqlalchemy.types import TypeDecorator, String, Binary from sqlalchemy_utils.exceptions import ImproperlyConfigured +from .scalar_coercible import ScalarCoercible cryptography = None try: @@ -111,7 +112,7 @@ class FernetEngine(EncryptionDecryptionBaseEngine): return decrypted -class EncryptedType(TypeDecorator): +class EncryptedType(TypeDecorator, ScalarCoercible): """ EncryptedType provides a way to encrypt and decrypt values, to and from databases, that their type is a basic SQLAlchemy type. @@ -223,3 +224,9 @@ class EncryptedType(TypeDecorator): self._update_key() decrypted_value = self.engine.decrypt(value) return self.underlying_type.python_type(decrypted_value) + + def _coerce(self, value): + if isinstance(self.underlying_type, ScalarCoercible): + return self.underlying_type._coerce(value) + + return value From 63c4b331dbd2d49b2ab50c50271a626376c8748d Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Thu, 30 Oct 2014 13:39:02 -0700 Subject: [PATCH 06/15] Remove comment. --- sqlalchemy_utils/types/encrypted.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlalchemy_utils/types/encrypted.py b/sqlalchemy_utils/types/encrypted.py index e0b0957..77e4634 100644 --- a/sqlalchemy_utils/types/encrypted.py +++ b/sqlalchemy_utils/types/encrypted.py @@ -182,7 +182,7 @@ class EncryptedType(TypeDecorator, ScalarCoercible): engine.dispose() """ - impl = Binary # CHANGE! + impl = Binary def __init__(self, type_in=None, key=None, engine=None, **kwargs): """Initialization.""" From 90efc25228702a9d08fd09165b1dd28360eae46c Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Thu, 30 Oct 2014 13:47:49 -0700 Subject: [PATCH 07/15] Try and use 'process_result_value' and 'process_bind_param' if possible --- sqlalchemy_utils/types/encrypted.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/sqlalchemy_utils/types/encrypted.py b/sqlalchemy_utils/types/encrypted.py index 77e4634..e3f000a 100644 --- a/sqlalchemy_utils/types/encrypted.py +++ b/sqlalchemy_utils/types/encrypted.py @@ -216,6 +216,15 @@ class EncryptedType(TypeDecorator, ScalarCoercible): """Encrypt a value on the way in.""" if value is not None: self._update_key() + + try: + value = self.underlying_type.process_bind_param( + value, dialect) + + except AttributeError: + # Doesn't have 'process_bind_param' + pass + return self.engine.encrypt(value) def process_result_value(self, value, dialect): @@ -223,7 +232,14 @@ class EncryptedType(TypeDecorator, ScalarCoercible): if value is not None: self._update_key() decrypted_value = self.engine.decrypt(value) - return self.underlying_type.python_type(decrypted_value) + + try: + return self.underlying_type.process_result_value( + decrypted_value, dialect) + + except AttributeError: + # Doesn't have 'process_result_value' + return self.underlying_type.python_type(decrypted_value) def _coerce(self, value): if isinstance(self.underlying_type, ScalarCoercible): From f1d6ba4ecc0c59f48ad6853d19a7041c600b5811 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Thu, 30 Oct 2014 13:57:27 -0700 Subject: [PATCH 08/15] Handle 'sa.Boolean' for 'EncryptedType' --- sqlalchemy_utils/types/encrypted.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/sqlalchemy_utils/types/encrypted.py b/sqlalchemy_utils/types/encrypted.py index e3f000a..4f5ec29 100644 --- a/sqlalchemy_utils/types/encrypted.py +++ b/sqlalchemy_utils/types/encrypted.py @@ -223,8 +223,12 @@ class EncryptedType(TypeDecorator, ScalarCoercible): except AttributeError: # Doesn't have 'process_bind_param' - pass + # Handle 'boolean' + if issubclass(self.underlying_type.python_type, bool): + value = "true" if value else "false" + + print("encrypt: ", value) return self.engine.encrypt(value) def process_result_value(self, value, dialect): @@ -239,6 +243,12 @@ class EncryptedType(TypeDecorator, ScalarCoercible): except AttributeError: # Doesn't have 'process_result_value' + + # Handle 'boolean' + if issubclass(self.underlying_type.python_type, bool): + return decrypted_value == "true" + + # Handle all others return self.underlying_type.python_type(decrypted_value) def _coerce(self, value): From 04d612a185253dd1a697937893c3d68408a5fb51 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Thu, 30 Oct 2014 14:10:54 -0700 Subject: [PATCH 09/15] Remove print --- sqlalchemy_utils/types/encrypted.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sqlalchemy_utils/types/encrypted.py b/sqlalchemy_utils/types/encrypted.py index 4f5ec29..178033d 100644 --- a/sqlalchemy_utils/types/encrypted.py +++ b/sqlalchemy_utils/types/encrypted.py @@ -228,7 +228,6 @@ class EncryptedType(TypeDecorator, ScalarCoercible): if issubclass(self.underlying_type.python_type, bool): value = "true" if value else "false" - print("encrypt: ", value) return self.engine.encrypt(value) def process_result_value(self, value, dialect): From cae6d5431d002b68940e2a60068aca8938cef681 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Thu, 30 Oct 2014 14:16:57 -0700 Subject: [PATCH 10/15] Handle date, time, and datetime column types. --- sqlalchemy_utils/types/encrypted.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/sqlalchemy_utils/types/encrypted.py b/sqlalchemy_utils/types/encrypted.py index 178033d..51b2ad8 100644 --- a/sqlalchemy_utils/types/encrypted.py +++ b/sqlalchemy_utils/types/encrypted.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import base64 import six +import datetime from sqlalchemy.types import TypeDecorator, String, Binary from sqlalchemy_utils.exceptions import ImproperlyConfigured from .scalar_coercible import ScalarCoercible @@ -224,10 +225,14 @@ class EncryptedType(TypeDecorator, ScalarCoercible): except AttributeError: # Doesn't have 'process_bind_param' - # Handle 'boolean' - if issubclass(self.underlying_type.python_type, bool): + # Handle 'boolean' and 'dates' + type_ = self.underlying_type.python_type + if issubclass(type_, bool): value = "true" if value else "false" + elif issubclass(type_, (datetime.date, datetime.time)): + value = value.isoformat() + return self.engine.encrypt(value) def process_result_value(self, value, dialect): @@ -243,10 +248,23 @@ class EncryptedType(TypeDecorator, ScalarCoercible): except AttributeError: # Doesn't have 'process_result_value' - # Handle 'boolean' - if issubclass(self.underlying_type.python_type, bool): + # Handle 'boolean' and 'dates' + type_ = self.underlying_type.python_type + if issubclass(type_, bool): return decrypted_value == "true" + elif issubclass(type_, datetime.time): + return datetime.datetime.strptime( + decrypted_value, "%H:%M:%S").time() + + elif issubclass(type_, datetime.date): + return datetime.datetime.strptime( + decrypted_value, "%Y-%m-%d").date() + + elif issubclass(type_, datetime.datetime): + return datetime.datetime.strptime( + decrypted_value, "%Y-%m-%dT%H:%M:%S") + # Handle all others return self.underlying_type.python_type(decrypted_value) From 65694782ac41960e948a2b8698e4ae9e3595461e Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Thu, 11 Dec 2014 13:45:02 -0800 Subject: [PATCH 11/15] Update tests, docs, and changelog --- CHANGES.rst | 3 +- sqlalchemy_utils/types/encrypted.py | 26 ++- tests/types/test_encrypted.py | 246 +++++++++++++++++----------- 3 files changed, 174 insertions(+), 101 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 65e0806..1f05270 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,7 +8,8 @@ Here you can see the full list of changes between each SQLAlchemy-Utils release. ^^^^^^^^^^^^^^^^^^^^ - Fixed PhoneNumber string coercion (#93) - +- Improved EncryptedType to support more underlying_type's; now supports: Integer, Boolean, Date, Time, DateTime, ColorType, PhoneNumberType, Unicode(Text), String(Text), Enum +- Allow a callable to be used to lookup the key for EncryptedType 0.27.11 (2014-12-06) ^^^^^^^^^^^^^^^^^^^^ diff --git a/sqlalchemy_utils/types/encrypted.py b/sqlalchemy_utils/types/encrypted.py index 51b2ad8..12e399f 100644 --- a/sqlalchemy_utils/types/encrypted.py +++ b/sqlalchemy_utils/types/encrypted.py @@ -181,6 +181,20 @@ class EncryptedType(TypeDecorator, ScalarCoercible): Base.metadata.drop_all(connection) connection.close() engine.dispose() + + The key parameter accepts a callable to allow for the key to change + per-row instead of be fixed for the whole table. + + :: + def get_key(): + return "dynamic-key" + + class User(Base): + __tablename__ = "user" + id = sa.Column(sa.Integer, primary_key=True) + username = sa.Column(EncryptedType( + sa.Unicode, get_key)) + """ impl = Binary @@ -195,7 +209,9 @@ class EncryptedType(TypeDecorator, ScalarCoercible): # set the underlying type if type_in is None: type_in = String() - self.underlying_type = type_in() + elif isinstance(type_in, type): + type_in = type_in() + self.underlying_type = type_in self._key = key if not engine: engine = AesEngine @@ -253,6 +269,10 @@ class EncryptedType(TypeDecorator, ScalarCoercible): if issubclass(type_, bool): return decrypted_value == "true" + elif issubclass(type_, datetime.datetime): + return datetime.datetime.strptime( + decrypted_value, "%Y-%m-%dT%H:%M:%S") + elif issubclass(type_, datetime.time): return datetime.datetime.strptime( decrypted_value, "%H:%M:%S").time() @@ -261,10 +281,6 @@ class EncryptedType(TypeDecorator, ScalarCoercible): return datetime.datetime.strptime( decrypted_value, "%Y-%m-%d").date() - elif issubclass(type_, datetime.datetime): - return datetime.datetime.strptime( - decrypted_value, "%Y-%m-%dT%H:%M:%S") - # Handle all others return self.underlying_type.python_type(decrypted_value) diff --git a/tests/types/test_encrypted.py b/tests/types/test_encrypted.py index 002bc9b..c762a6f 100644 --- a/tests/types/test_encrypted.py +++ b/tests/types/test_encrypted.py @@ -1,4 +1,6 @@ import sqlalchemy as sa +from datetime import datetime, date, time +import pytest from pytest import mark cryptography = None try: @@ -7,42 +9,67 @@ except ImportError: pass from tests import TestCase -from sqlalchemy_utils import EncryptedType +from sqlalchemy_utils import EncryptedType, PhoneNumberType, ColorType from sqlalchemy_utils.types.encrypted import AesEngine, FernetEngine @mark.skipif('cryptography is None') class EncryptedTypeTestCase(TestCase): - def setup_method(self, method): - # set some test values - self.test_key = 'secretkey1234' - self.user_name = u'someone' - self.test_token = self.generate_test_token() - self.active = True - self.accounts_num = 2 - self.searched_user = None - super(EncryptedTypeTestCase, self).setup_method(method) + + @pytest.fixture(scope="function") + def user(self, request): # set the values to the user object self.user = self.User() self.user.username = self.user_name + self.user.phone = self.user_phone + self.user.color = self.user_color + self.user.date = self.user_date + self.user.time = self.user_time + self.user.enum = self.user_enum + self.user.datetime = self.user_datetime self.user.access_token = self.test_token self.user.is_active = self.active self.user.accounts_num = self.accounts_num self.session.add(self.user) self.session.commit() - def teardown_method(self, method): - self.session.delete(self.user) - self.session.commit() - del self.user_name - del self.test_token - del self.active - del self.accounts_num - del self.test_key - del self.searched_user - super(EncryptedTypeTestCase, self).teardown_method(method) + # register a finalizer to cleanup + def finalize(): + del self.user_name + del self.test_token + del self.active + del self.accounts_num + del self.test_key + del self.searched_user + + request.addfinalizer(finalize) + + return self.session.query(self.User).get(self.user.id) + + def generate_test_token(self): + import string + import random + token = "" + characters = string.ascii_letters + string.digits + for i in range(60): + token += ''.join(random.choice(characters)) + return token def create_models(self): + # set some test values + self.test_key = 'secretkey1234' + self.user_name = u'someone' + self.user_phone = u'(555) 555-5555' + self.user_color = u'#fff' + self.user_enum = "One" + self.user_date = date(2010, 10, 2) + self.user_time = time(10, 12) + self.user_datetime = datetime(2010, 10, 2, 10, 12) + self.test_token = self.generate_test_token() + self.active = True + self.accounts_num = 2 + self.searched_user = None + class User(self.Base): __tablename__ = 'user' id = sa.Column(sa.Integer, primary_key=True) @@ -62,96 +89,125 @@ class EncryptedTypeTestCase(TestCase): sa.Integer, self.test_key, self.__class__.encryption_engine)) - - def __repr__(self): - return ( - "User(id={}, username={}, access_token={}," - "active={}, accounts={})".format( - self.id, - self.username, - self.access_token, - self.is_active, - self.accounts_num - ) - ) + phone = sa.Column(EncryptedType( + PhoneNumberType, + self.test_key, + self.__class__.encryption_engine)) + color = sa.Column(EncryptedType( + ColorType, + self.test_key, + self.__class__.encryption_engine)) + date = sa.Column(EncryptedType( + sa.Date, + self.test_key, + self.__class__.encryption_engine)) + time = sa.Column(EncryptedType( + sa.Time, + self.test_key, + self.__class__.encryption_engine)) + datetime = sa.Column(EncryptedType( + sa.DateTime, + self.test_key, + self.__class__.encryption_engine)) + enum = sa.Column(EncryptedType( + sa.Enum("One", name="user_enum_t"), + self.test_key, + self.__class__.encryption_engine)) self.User = User - def assert_username(self, _user): - assert _user.username == self.user_name + class Team(self.Base): + __tablename__ = 'team' + id = sa.Column(sa.Integer, primary_key=True) + key = sa.Column(sa.Unicode(50)) + name = sa.Column(EncryptedType( + sa.Unicode, + lambda: self._team_key, + self.__class__.encryption_engine)) - def assert_access_token(self, _user): - assert _user.access_token == self.test_token + self.Team = Team - def assert_is_active(self, _user): - assert _user.is_active == self.active + def test_unicode(self, user): + assert user.username == self.user_name - def assert_accounts_num(self, _user): - assert _user.accounts_num == self.accounts_num + def test_string(self, user): + assert user.access_token == self.test_token - def generate_test_token(self): - import string - import random - token = "" - characters = string.ascii_letters + string.digits - for i in range(60): - token += ''.join(random.choice(characters)) - return token + def test_boolean(self, user): + assert user.is_active == self.active + + def test_integer(self, user): + assert user.accounts_num == self.accounts_num + + def test_phone_number(self, user): + assert str(user.phone) == self.user_phone + + def test_color(self, user): + assert user.color.hex == self.user_color + + def test_date(self, user): + assert user.date == self.user_date + + def test_datetime(self, user): + assert user.datetime == self.user_datetime + + def test_time(self, user): + assert user.time == self.user_time + + def test_enum(self, user): + assert user.enum == self.user_enum + + def test_lookup_key(self): + # Add teams + self._team_key = "one" + team = self.Team(key=self._team_key, name="One") + self.session.add(team) + self.session.flush() + team_1_id = team.id + + self._team_key = "two" + team = self.Team(key=self._team_key, name="Two") + self.session.add(team) + self.session.flush() + team_2_id = team.id + + self.session.commit() + + # Lookup teams + self._team_key = self.session.query(self.Team.key).filter_by( + id=team_1_id).one()[0] + team = self.session.query(self.Team).get(team_1_id) + + assert team.name == "One" + + with pytest.raises(Exception): + self.session.query(self.Team).get(team_2_id) + + self._team_key = self.session.query(self.Team.key).filter_by( + id=team_2_id).one()[0] + team = self.session.query(self.Team).get(team_2_id) + + assert team.name == "Two" + + with pytest.raises(Exception): + self.session.query(self.Team).get(team_1_id) + + # Remove teams + self.session.query(self.Team).delete() + self.session.commit() class TestAesEncryptedTypeTestcase(EncryptedTypeTestCase): encryption_engine = AesEngine - def test_unicode(self): - self.searched_user = self.session.query(self.User).filter( - self.User.access_token == self.test_token - ).first() - self.assert_username(self.searched_user) + def test_lookup_by_encrypted_string(self, user): + test = self.session.query(self.User).filter( + self.User.username == self.user_name).first() - def test_string(self): - self.searched_user = self.session.query(self.User).filter( - self.User.username == self.user_name - ).first() - self.assert_access_token(self.searched_user) - - def test_boolean(self): - self.searched_user = self.session.query(self.User).filter( - self.User.access_token == self.test_token - ).first() - self.assert_is_active(self.searched_user) - - def test_integer(self): - self.searched_user = self.session.query(self.User).filter( - self.User.access_token == self.test_token - ).first() - self.assert_accounts_num(self.searched_user) + assert test.username == user.username -class TestFernetEnryptedTypeTestCase(EncryptedTypeTestCase): +class TestFernetEncryptedTypeTestCase(EncryptedTypeTestCase): encryption_engine = FernetEngine - - def test_unicode(self): - self.searched_user = self.session.query(self.User).filter( - self.User.id == self.user.id - ).first() - self.assert_username(self.searched_user) - - def test_string(self): - self.searched_user = self.session.query(self.User).filter( - self.User.id == self.user.id - ).first() - self.assert_access_token(self.searched_user) - - def test_boolean(self): - self.searched_user = self.session.query(self.User).filter( - self.User.id == self.user.id - ).first() - self.assert_is_active(self.searched_user) - - def test_integer(self): - self.searched_user = self.session.query(self.User).filter( - self.User.id == self.user.id - ).first() - self.assert_accounts_num(self.searched_user) From 3a2991b2212bc5a517dc45b4ebd81269283c1500 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Fri, 12 Dec 2014 10:05:00 -0800 Subject: [PATCH 12/15] Upgrade pytest (for fixtures) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b34cf1d..a321aa1 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ PY3 = sys.version_info[0] == 3 extras_require = { 'test': [ - 'pytest==2.2.3', + 'pytest==2.3.5', 'Pygments>=1.2', 'Jinja2>=2.3', 'docutils>=0.10', From 8e36f3d6b19aa67df546654a225b01b0d862b647 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Fri, 12 Dec 2014 10:05:34 -0800 Subject: [PATCH 13/15] Update to have a consistent style --- sqlalchemy_utils/types/encrypted.py | 19 +++++--- tests/types/test_encrypted.py | 74 +++++++++++++++++++---------- 2 files changed, 62 insertions(+), 31 deletions(-) diff --git a/sqlalchemy_utils/types/encrypted.py b/sqlalchemy_utils/types/encrypted.py index 12e399f..105e0bf 100644 --- a/sqlalchemy_utils/types/encrypted.py +++ b/sqlalchemy_utils/types/encrypted.py @@ -236,7 +236,8 @@ class EncryptedType(TypeDecorator, ScalarCoercible): try: value = self.underlying_type.process_bind_param( - value, dialect) + value, dialect + ) except AttributeError: # Doesn't have 'process_bind_param' @@ -244,7 +245,7 @@ class EncryptedType(TypeDecorator, ScalarCoercible): # Handle 'boolean' and 'dates' type_ = self.underlying_type.python_type if issubclass(type_, bool): - value = "true" if value else "false" + value = 'true' if value else 'false' elif issubclass(type_, (datetime.date, datetime.time)): value = value.isoformat() @@ -259,7 +260,8 @@ class EncryptedType(TypeDecorator, ScalarCoercible): try: return self.underlying_type.process_result_value( - decrypted_value, dialect) + decrypted_value, dialect + ) except AttributeError: # Doesn't have 'process_result_value' @@ -267,19 +269,22 @@ class EncryptedType(TypeDecorator, ScalarCoercible): # Handle 'boolean' and 'dates' type_ = self.underlying_type.python_type if issubclass(type_, bool): - return decrypted_value == "true" + return decrypted_value == 'true' elif issubclass(type_, datetime.datetime): return datetime.datetime.strptime( - decrypted_value, "%Y-%m-%dT%H:%M:%S") + decrypted_value, '%Y-%m-%dT%H:%M:%S' + ) elif issubclass(type_, datetime.time): return datetime.datetime.strptime( - decrypted_value, "%H:%M:%S").time() + decrypted_value, '%H:%M:%S' + ).time() elif issubclass(type_, datetime.date): return datetime.datetime.strptime( - decrypted_value, "%Y-%m-%d").date() + decrypted_value, '%Y-%m-%d' + ).date() # Handle all others return self.underlying_type.python_type(decrypted_value) diff --git a/tests/types/test_encrypted.py b/tests/types/test_encrypted.py index c762a6f..cef9dd5 100644 --- a/tests/types/test_encrypted.py +++ b/tests/types/test_encrypted.py @@ -16,7 +16,7 @@ from sqlalchemy_utils.types.encrypted import AesEngine, FernetEngine @mark.skipif('cryptography is None') class EncryptedTypeTestCase(TestCase): - @pytest.fixture(scope="function") + @pytest.fixture(scope='function') def user(self, request): # set the values to the user object self.user = self.User() @@ -49,7 +49,7 @@ class EncryptedTypeTestCase(TestCase): def generate_test_token(self): import string import random - token = "" + token = '' characters = string.ascii_letters + string.digits for i in range(60): token += ''.join(random.choice(characters)) @@ -61,7 +61,7 @@ class EncryptedTypeTestCase(TestCase): self.user_name = u'someone' self.user_phone = u'(555) 555-5555' self.user_color = u'#fff' - self.user_enum = "One" + self.user_enum = 'One' self.user_date = date(2010, 10, 2) self.user_time = time(10, 12) self.user_datetime = datetime(2010, 10, 2, 10, 12) @@ -73,46 +73,66 @@ class EncryptedTypeTestCase(TestCase): class User(self.Base): __tablename__ = 'user' id = sa.Column(sa.Integer, primary_key=True) + username = sa.Column(EncryptedType( sa.Unicode, self.test_key, - self.__class__.encryption_engine)) + self.__class__.encryption_engine) + ) + access_token = sa.Column(EncryptedType( sa.String, self.test_key, - self.__class__.encryption_engine)) + self.__class__.encryption_engine) + ) + is_active = sa.Column(EncryptedType( sa.Boolean, self.test_key, - self.__class__.encryption_engine)) + self.__class__.encryption_engine) + ) + accounts_num = sa.Column(EncryptedType( sa.Integer, self.test_key, - self.__class__.encryption_engine)) + self.__class__.encryption_engine) + ) + phone = sa.Column(EncryptedType( PhoneNumberType, self.test_key, - self.__class__.encryption_engine)) + self.__class__.encryption_engine) + ) + color = sa.Column(EncryptedType( ColorType, self.test_key, - self.__class__.encryption_engine)) + self.__class__.encryption_engine) + ) + date = sa.Column(EncryptedType( sa.Date, self.test_key, - self.__class__.encryption_engine)) + self.__class__.encryption_engine) + ) + time = sa.Column(EncryptedType( sa.Time, self.test_key, - self.__class__.encryption_engine)) + self.__class__.encryption_engine) + ) + datetime = sa.Column(EncryptedType( sa.DateTime, self.test_key, - self.__class__.encryption_engine)) + self.__class__.encryption_engine) + ) + enum = sa.Column(EncryptedType( - sa.Enum("One", name="user_enum_t"), + sa.Enum('One', name='user_enum_t'), self.test_key, - self.__class__.encryption_engine)) + self.__class__.encryption_engine) + ) self.User = User @@ -123,7 +143,8 @@ class EncryptedTypeTestCase(TestCase): name = sa.Column(EncryptedType( sa.Unicode, lambda: self._team_key, - self.__class__.encryption_engine)) + self.__class__.encryption_engine) + ) self.Team = Team @@ -159,14 +180,14 @@ class EncryptedTypeTestCase(TestCase): def test_lookup_key(self): # Add teams - self._team_key = "one" - team = self.Team(key=self._team_key, name="One") + self._team_key = 'one' + team = self.Team(key=self._team_key, name='One') self.session.add(team) self.session.flush() team_1_id = team.id - self._team_key = "two" - team = self.Team(key=self._team_key, name="Two") + self._team_key = 'two' + team = self.Team(key=self._team_key, name='Two') self.session.add(team) self.session.flush() team_2_id = team.id @@ -175,19 +196,23 @@ class EncryptedTypeTestCase(TestCase): # Lookup teams self._team_key = self.session.query(self.Team.key).filter_by( - id=team_1_id).one()[0] + id=team_1_id + ).one()[0] + team = self.session.query(self.Team).get(team_1_id) - assert team.name == "One" + assert team.name == 'One' with pytest.raises(Exception): self.session.query(self.Team).get(team_2_id) self._team_key = self.session.query(self.Team.key).filter_by( - id=team_2_id).one()[0] + id=team_2_id + ).one()[0] + team = self.session.query(self.Team).get(team_2_id) - assert team.name == "Two" + assert team.name == 'Two' with pytest.raises(Exception): self.session.query(self.Team).get(team_1_id) @@ -203,7 +228,8 @@ class TestAesEncryptedTypeTestcase(EncryptedTypeTestCase): def test_lookup_by_encrypted_string(self, user): test = self.session.query(self.User).filter( - self.User.username == self.user_name).first() + self.User.username == self.user_name + ).first() assert test.username == user.username From 15dcbb10549f70b03a282da1a62f170308c1eb82 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Fri, 12 Dec 2014 10:08:14 -0800 Subject: [PATCH 14/15] Make code in docstrings use single-quotes as well --- sqlalchemy_utils/types/encrypted.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlalchemy_utils/types/encrypted.py b/sqlalchemy_utils/types/encrypted.py index 105e0bf..2446348 100644 --- a/sqlalchemy_utils/types/encrypted.py +++ b/sqlalchemy_utils/types/encrypted.py @@ -187,10 +187,10 @@ class EncryptedType(TypeDecorator, ScalarCoercible): :: def get_key(): - return "dynamic-key" + return 'dynamic-key' class User(Base): - __tablename__ = "user" + __tablename__ = 'user' id = sa.Column(sa.Integer, primary_key=True) username = sa.Column(EncryptedType( sa.Unicode, get_key)) From d21e400a8bebfdf3e8f9574f156edce7184de19a Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Fri, 12 Dec 2014 11:19:12 -0800 Subject: [PATCH 15/15] Fix for python 2.x --- sqlalchemy_utils/types/encrypted.py | 2 +- tests/types/test_encrypted.py | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/sqlalchemy_utils/types/encrypted.py b/sqlalchemy_utils/types/encrypted.py index 2446348..0d28b41 100644 --- a/sqlalchemy_utils/types/encrypted.py +++ b/sqlalchemy_utils/types/encrypted.py @@ -28,7 +28,7 @@ class EncryptionDecryptionBaseEngine(object): def _update_key(self, key): if isinstance(key, six.string_types): - key = six.b(key) + key = key.encode() digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) digest.update(key) engine_key = digest.finalize() diff --git a/tests/types/test_encrypted.py b/tests/types/test_encrypted.py index cef9dd5..d5ca19c 100644 --- a/tests/types/test_encrypted.py +++ b/tests/types/test_encrypted.py @@ -139,7 +139,7 @@ class EncryptedTypeTestCase(TestCase): class Team(self.Base): __tablename__ = 'team' id = sa.Column(sa.Integer, primary_key=True) - key = sa.Column(sa.Unicode(50)) + key = sa.Column(sa.String(50)) name = sa.Column(EncryptedType( sa.Unicode, lambda: self._team_key, @@ -181,18 +181,17 @@ class EncryptedTypeTestCase(TestCase): def test_lookup_key(self): # Add teams self._team_key = 'one' - team = self.Team(key=self._team_key, name='One') + team = self.Team(key=self._team_key, name=u'One') self.session.add(team) - self.session.flush() + self.session.commit() team_1_id = team.id self._team_key = 'two' - team = self.Team(key=self._team_key, name='Two') + team = self.Team(key=self._team_key) + team.name = u'Two' self.session.add(team) - self.session.flush() - team_2_id = team.id - self.session.commit() + team_2_id = team.id # Lookup teams self._team_key = self.session.query(self.Team.key).filter_by( @@ -201,22 +200,26 @@ class EncryptedTypeTestCase(TestCase): team = self.session.query(self.Team).get(team_1_id) - assert team.name == 'One' + assert team.name == u'One' with pytest.raises(Exception): self.session.query(self.Team).get(team_2_id) + self.session.expunge_all() + self._team_key = self.session.query(self.Team.key).filter_by( id=team_2_id ).one()[0] team = self.session.query(self.Team).get(team_2_id) - assert team.name == 'Two' + assert team.name == u'Two' with pytest.raises(Exception): self.session.query(self.Team).get(team_1_id) + self.session.expunge_all() + # Remove teams self.session.query(self.Team).delete() self.session.commit()