]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Ensure str is callect on the URL password.
authorFederico Caselli <cfederico87@gmail.com>
Wed, 8 Sep 2021 20:25:12 +0000 (22:25 +0200)
committerFederico Caselli <cfederico87@gmail.com>
Mon, 13 Sep 2021 19:23:10 +0000 (21:23 +0200)
Ensure that ``str()`` is called on the an ``URL.password`` argument,
allowing usage of objects that implement the ``__str__()`` method
as password attributes.
Also clarified that one such object is not appropriate to dynamically
change the password.

Fixes: #6958
Change-Id: Id0690990a64b9e0935537b7b8f5a73efe6a9e3dc

doc/build/changelog/unreleased_14/6958.rst [new file with mode: 0644]
lib/sqlalchemy/engine/events.py
lib/sqlalchemy/engine/url.py
test/engine/test_parseconnect.py

diff --git a/doc/build/changelog/unreleased_14/6958.rst b/doc/build/changelog/unreleased_14/6958.rst
new file mode 100644 (file)
index 0000000..61fd141
--- /dev/null
@@ -0,0 +1,9 @@
+.. change::
+    :tags: bug, engine
+    :tickets: 6958
+
+    Ensure that ``str()`` is called on the an ``URL.password`` argument,
+    allowing usage of objects that implement the ``__str__()`` method
+    as password attributes.
+    Also clarified that one such object is not appropriate to dynamically
+    change the password.
index f3775aed7940f324ed8e1ced3e17a981d897f6f5..f091c7733a826d20c04fc2cd48889a9cd9cf8231 100644 (file)
@@ -722,15 +722,27 @@ class DialectEvents(event.Events):
     def do_connect(self, dialect, conn_rec, cargs, cparams):
         """Receive connection arguments before a connection is made.
 
-        Return a DBAPI connection to halt further events from invoking;
-        the returned connection will be used.
-
-        Alternatively, the event can manipulate the cargs and/or cparams
-        collections; cargs will always be a Python list that can be mutated
-        in-place and cparams a Python dictionary.  Return None to
-        allow control to pass to the next event handler and ultimately
-        to allow the dialect to connect normally, given the updated
-        arguments.
+        This event is useful in that it allows the handler to manipulate the
+        cargs and/or cparams collections that control how the DBAPI
+        ``connect()`` function will be called. ``cargs`` will always be a
+        Python list that can be mutated in-place, and ``cparams`` a Python
+        dictionary that may also be mutated::
+
+            e = create_engine("postgresql+psycopg2://user@host/dbname")
+
+            @event.listens_for(e, 'do_connect')
+            def receive_do_connect(dialect, conn_rec, cargs, cparams):
+                cparams["password"] = "some_password"
+
+        The event hook may also be used to override the call to ``connect()``
+        entirely, by returning a non-``None`` DBAPI connection object::
+
+            e = create_engine("postgresql+psycopg2://user@host/dbname")
+
+            @event.listens_for(e, 'do_connect')
+            def receive_do_connect(dialect, conn_rec, cargs, cparams):
+                return psycopg2.connect(*cargs, **cparams)
+
 
         .. versionadded:: 1.0.3
 
index 1b96c3c2e1790c100c64b9f6091b5b4436b55eee..d91f0601138d9bb5a4ab1169007ed9e8dde5cba2 100644 (file)
@@ -67,8 +67,13 @@ class URL(
     * :attr:`_engine.URL.drivername`: database backend and driver name, such as
       ``postgresql+psycopg2``
     * :attr:`_engine.URL.username`: username string
-    * :attr:`_engine.URL.password`: password, which is normally a string but
-      may also be any object that has a ``__str__()`` method.
+    * :attr:`_engine.URL.password`: password string, or object that includes
+      a ``__str__()`` method that produces a password.
+
+      .. note::  A password-producing object will be stringified only
+         **once** per :class:`_engine.Engine` object.  For dynamic password
+         generation per connect, see :ref:`engines_dynamic_tokens`.
+
     * :attr:`_engine.URL.host`: string hostname
     * :attr:`_engine.URL.port`: integer port number
     * :attr:`_engine.URL.database`: string database name
@@ -108,8 +113,13 @@ class URL(
           correspond to a module in sqlalchemy/databases or a third party
           plug-in.
         :param username: The user name.
-        :param password: database password.  May be a string or an object that
-         can be stringified with ``str()``.
+        :param password: database password.  Is typically a string, but may
+          also be an object that can be stringified with ``str()``.
+
+          .. note::  A password-producing object will be stringified only
+             **once** per :class:`_engine.Engine` object.  For dynamic password
+             generation per connect, see :ref:`engines_dynamic_tokens`.
+
         :param host: The name of the host.
         :param port: The port number.
         :param database: The database name.
@@ -666,6 +676,14 @@ class URL(
             names, but correlates the name to the original positionally.
         """
 
+        if names is not None:
+            util.warn_deprecated(
+                "The `URL.translate_connect_args.name`s parameter is "
+                "deprecated. Please pass the "
+                "alternate names as kw arguments.",
+                "1.4",
+            )
+
         translated = {}
         attribute_names = ["host", "database", "username", "password", "port"]
         for sname in attribute_names:
@@ -676,7 +694,11 @@ class URL(
             else:
                 name = sname
             if name is not None and getattr(self, sname, False):
-                translated[name] = getattr(self, sname)
+                if sname == "password":
+                    translated[name] = str(getattr(self, sname))
+                else:
+                    translated[name] = getattr(self, sname)
+
         return translated
 
 
index 9acedaa8526e3081ac7d54409bd4ac8af59ab211..136695279aa3236f919f88f910fa47685d65ac21 100644 (file)
@@ -367,6 +367,16 @@ class URLTest(fixtures.TestBase):
             )
         eq_(u1, url.make_url("somedriver://user@hostname:52"))
 
+    def test_deprecated_translate_connect_args_names(self):
+        u = url.make_url("somedriver://user@hostname:52")
+
+        with testing.expect_deprecated(
+            "The `URL.translate_connect_args.name`s parameter is "
+        ):
+            res = u.translate_connect_args(["foo"])
+            is_true("foo" in res)
+            eq_(res["foo"], u.host)
+
 
 class DialectImportTest(fixtures.TestBase):
     def test_import_base_dialects(self):
@@ -691,6 +701,43 @@ class CreateEngineTest(fixtures.TestBase):
             _initialize=False,
         )
 
+    @testing.combinations(True, False)
+    def test_password_object_str(self, creator):
+        class SecurePassword:
+            def __init__(self, value):
+                self.called = 0
+                self.value = value
+
+            def __str__(self):
+                self.called += 1
+                return self.value
+
+        sp = SecurePassword("secured_password")
+        u = url.URL.create(
+            "postgresql", username="x", password=sp, host="localhost"
+        )
+        if not creator:
+            dbapi = MockDBAPI(
+                user="x", password="secured_password", host="localhost"
+            )
+
+            e = create_engine(u, module=dbapi, _initialize=False)
+
+        else:
+            dbapi = MockDBAPI(foober=12, lala=18, fooz="somevalue")
+
+            def connect():
+                return dbapi.connect(foober=12, lala=18, fooz="somevalue")
+
+            e = create_engine(
+                u, creator=connect, module=dbapi, _initialize=False
+            )
+        e.connect()
+        e.connect()
+        e.connect()
+        e.connect()
+        eq_(sp.called, 1)
+
 
 class TestRegNewDBAPI(fixtures.TestBase):
     def test_register_base(self):