Bug with hybrid property ?

Issue #3965 closed
Olivier Le Moign created an issue

I use SQLALchemy ORM with a declarative base and SQLITE. I use hybrid properties to encrypt TOTP secrets as follows:

class FactorsTOTP(Model):
    app_secret = 'foobar'

    user_id = Column(Integer, ForeignKey('user.id'), nullable=False)
    _key = Column('key', String, nullable=False)

    @hybrid_property
    def key(self):
        cipher_key = urlsafe_b64encode(self.app_secret.ljust(32, b'0'))
        f = Fernet(cipher_key)
        return f.decrypt(self._key.encode('utf-8'))

    @key.setter
    def key(self, value):
        cipher_key = urlsafe_b64encode(self.app_secret.ljust(32, b'0'))
        f = Fernet(cipher_key)
        self._key = f.encrypt(value.encode('utf-8')).decode('ascii')

My problem is, when creating a row: factors_totp = models.FactorsTOTP(key=totp_key)

I'm not sure why, but the key getter is being called after the setter and fails on self._key.encode('utf-8'). Simple Python operations work (self._key + 'suffix'), but not calling a function (bytes(self.key, encoding='utf-8) fails as well).

Traceback (most recent call last):
  File "/Users/olemoign/.pyenv/versions/rta/bin/rta_add_admin", line 11, in <module>
    load_entry_point('rta', 'console_scripts', 'rta_add_admin')()
  File "/Users/olemoign/Developer/Parsys/rta/rta/scripts/add_admin.py", line 27, in main
    password, totp_key = _command_line_add_user(env['request'], admin)
  File "/Users/olemoign/Developer/Parsys/rta/rta/services/users.py", line 363, in _command_line_add_user
    factors_totp = models.FactorsTOTP(key=totp_key)
  File "<string>", line 4, in __init__
  File "/Users/olemoign/.pyenv/versions/3.5.2/envs/rta/lib/python3.5/site-packages/sqlalchemy/orm/state.py", line 414, in _initialize_instance
    manager.dispatch.init_failure(self, args, kwargs)
  File "/Users/olemoign/.pyenv/versions/3.5.2/envs/rta/lib/python3.5/site-packages/sqlalchemy/util/langhelpers.py", line 66, in __exit__
    compat.reraise(exc_type, exc_value, exc_tb)
  File "/Users/olemoign/.pyenv/versions/3.5.2/envs/rta/lib/python3.5/site-packages/sqlalchemy/util/compat.py", line 187, in reraise
    raise value
  File "/Users/olemoign/.pyenv/versions/3.5.2/envs/rta/lib/python3.5/site-packages/sqlalchemy/orm/state.py", line 411, in _initialize_instance
    return manager.original_init(*mixed[1:], **kwargs)
  File "/Users/olemoign/.pyenv/versions/3.5.2/envs/rta/lib/python3.5/site-packages/sqlalchemy/ext/declarative/base.py", line 653, in _declarative_constructor
    (k, cls_.__name__))
TypeError: 'key' is an invalid keyword argument for FactorsTOTP

This isn't so bad, as I can do

factors_totp = models.FactorsTOTP() factors_totp.key = totp_key

but I just wanted to raise the issue. Thank you for your great library.

Comments (3)

  1. Mike Bayer repo owner

    hello -

    your hybrid needs to provide an @expression so that it works at the class level as well, e.g. one could say "FactorsOTP.key". This expression is failing right now because of some combination of "self.app_secret" doesn't exist and/or FactorsOTP._key is a column expression and has no method ".encode()".

    You may respond, "well my hybrid has no intention of being used as a SQL expression". Then this should not be a hybrid, use @property.

    no issue observed here...

  2. Olivier Le Moign reporter

    Very well ! In the end, I'm mostly surprised that the getter is called during initialisation, but I guess that's linked to SQLAlchemy internals that make it possible to set the attributes at init in the first place.

    Thanks for the super quick answer Michael !

  3. Mike Bayer repo owner

    yup, this is just the default constructor for declarative.init, you can add your own init to the class / base to override that happening

  4. Log in to comment