support passwords with `__str__()` object in engine.URL, maintain the object and work w/ all drivers

Issue #4089 resolved
Brian Heineman
created an issue

The engine_url.URL class makes use of the ord() function which cannot be overrridden. This is preventing the direct use of overridden str values. Here is a test case that illustrates the issue:

from sqlalchemy.engine import url as engine_url


class SecurePassword(str):
    # any method that can be overridden by str to retrieve the value would be acceptable
    def __str__(self):
        return 'secured_password'


# The ord() function in _rfc_1738_quote() https://github.com/zzzeek/sqlalchemy/blob/master/lib/sqlalchemy/engine/url.py#L247
# is preventing the direct use of overridden strings
# https://stackoverflow.com/questions/1893816/how-to-override-ord-behaivour-in-python-for-str-childs
if __name__ == '__main__':
    password = SecurePassword('password_key')

    db_url = {
        'drivername': 'mysql',
        'host': 'localhost',
        'port': '3306',
        'username': 'root',
        'password': password,
        'database': 'test'
    }

    url = engine_url.URL(**db_url)
    print(url)
    assert str(url) == 'mysql://root:secured_password@localhost:3306/test'

Comments (10)

  1. Michael Bayer repo owner

    we need to ensure the spec here covers the use cases that would be needed. for example, the simplest fix here would be that URL just runs str() on the incoming value, so that url.password is again a string and not a SecurePassword object. If OTOH you wanted url.password to remain this special object then that needs to be made part of the behavioral contract that we include in testing.

  2. Michael Bayer repo owner

    OK, so we will probably say ord(str(password))for that one part of it, but then we need to add a bunch of tests that every function regarding URL works when the password is a non-string object with __str__().

  3. Michael Bayer repo owner

    not the least of which is that the drivers themselves accept a non-string for password, which I think is unlikely in all cases. so likely we will still have to str() this when passing onto the drivers.

  4. Michael Bayer repo owner

    OK this can work if the "url.password" accessor returns the string value, and not the object. for a while I was puzzled how to do this if url.password returned the object because handing it to a DBAPI, a lot of them would likely choke on it as well and I didn't want to stringify in the dialects.

    The proposal would add your object as the "url.password_original" data member and would be used to generate the string value:

    diff --git a/lib/sqlalchemy/engine/url.py b/lib/sqlalchemy/engine/url.py
    index 1ca5983fd..18b184878 100644
    --- a/lib/sqlalchemy/engine/url.py
    +++ b/lib/sqlalchemy/engine/url.py
    @@ -54,7 +54,7 @@ class URL(object):
                      host=None, port=None, database=None, query=None):
             self.drivername = drivername
             self.username = username
    -        self.password = password
    +        self.password_original = password
             self.host = host
             if port is not None:
                 self.port = int(port)
    @@ -105,6 +105,17 @@ class URL(object):
                 self.database == other.database and \
                 self.query == other.query
    
    +    @property
    +    def password(self):
    +        if self.password_original is None:
    +            return None
    +        else:
    +            return util.text_type(self.password_original)
    +
    +    @password.setter
    +    def password(self, password):
    +        self.password_original = password
    +
         def get_backend_name(self):
             if '+' not in self.drivername:
                 return self.drivername
    

    this does everything you'd said, it just would not allow you to make use of other methods of your object via the "url.password" attribute, such as if you wanted to say "url.password.special_hash" or something, you'd need to say "url.password_original.special_hash". Does that suit the full use case ?

  5. Michael Bayer repo owner

    as well as if you needed special comparison logic or soemthing like that, e.g. "url1.password == url2.password", and you wanted SecurePassword.eq() to be invoked, that wouldn't work either.

  6. Michael Bayer repo owner

    Allow url.password to be an object

    The "password" attribute of the :class:.url.URL object can now be any user-defined or user-subclassed string object that responds to the Python str() builtin. The object passed will be maintained as the datamember :attr:.url.URL.password_original and will be consulted when the :attr:.url.URL.password attribute is read to produce the string value.

    Change-Id: I91d101c3b10e135ae7e4de60a5104b51776db84f Fixes: #4089

    → <<cset e6438cf8c3d2>>

  7. Log in to comment