mirror of
				https://github.com/psycopg/psycopg2.git
				synced 2025-11-04 09:47:30 +03:00 
			
		
		
		
	Merge branch 'encrypt-pass'
This commit is contained in:
		
						commit
						f947c0e6be
					
				
							
								
								
									
										4
									
								
								NEWS
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								NEWS
									
									
									
									
									
								
							| 
						 | 
					@ -4,6 +4,10 @@ Current release
 | 
				
			||||||
What's new in psycopg 2.8
 | 
					What's new in psycopg 2.8
 | 
				
			||||||
-------------------------
 | 
					-------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					New features:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Added `~psycopg2.extensions.encrypt_password()` function (:ticket:`#576`).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Other changes:
 | 
					Other changes:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Dropped support for Python 2.6, 3.2, 3.3.
 | 
					- Dropped support for Python 2.6, 3.2, 3.3.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -555,6 +555,38 @@ Other functions
 | 
				
			||||||
        .. __: http://www.postgresql.org/docs/current/static/libpq-exec.html#LIBPQ-PQESCAPEIDENTIFIER
 | 
					        .. __: http://www.postgresql.org/docs/current/static/libpq-exec.html#LIBPQ-PQESCAPEIDENTIFIER
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. method:: encrypt_password(password, user, scope=None, algorithm=None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Return the encrypted form of a PostgreSQL password.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param password: the cleartext password to encrypt
 | 
				
			||||||
 | 
					    :param user: the name of the user to use the password for
 | 
				
			||||||
 | 
					    :param scope: the scope to encrypt the password into; if *algorithm* is
 | 
				
			||||||
 | 
					        ``md5`` it can be `!None`
 | 
				
			||||||
 | 
					    :type scope: `connection` or `cursor`
 | 
				
			||||||
 | 
					    :param algorithm: the password encryption algorithm to use
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The *algorithm* ``md5`` is always supported. Other algorithms are only
 | 
				
			||||||
 | 
					    supported if the client libpq version is at least 10 and may require a
 | 
				
			||||||
 | 
					    compatible server version: check the `PostgreSQL encryption
 | 
				
			||||||
 | 
					    documentation`__ to know the algorithms supported by your server.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. __: https://www.postgresql.org/docs/current/static/encryption-options.html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Using `!None` as *algorithm* will result in querying the server to know the
 | 
				
			||||||
 | 
					    current server password encryption setting, which is a blocking operation:
 | 
				
			||||||
 | 
					    query the server separately and specify a value for *algorithm* if you
 | 
				
			||||||
 | 
					    want to maintain a non-blocking behaviour.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. versionadded:: 2.8
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. seealso:: PostgreSQL docs for the `password_encryption`__ setting, libpq `PQencryptPasswordConn()`__, `PQencryptPassword()`__ functions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .. __: https://www.postgresql.org/docs/current/static/runtime-config-connection.html#GUC-PASSWORD-ENCRYPTION
 | 
				
			||||||
 | 
					        .. __: https://www.postgresql.org/docs/current/static/libpq-misc.html#LIBPQ-PQENCRYPTPASSWORDCONN
 | 
				
			||||||
 | 
					        .. __: https://www.postgresql.org/docs/current/static/libpq-misc.html#LIBPQ-PQENCRYPTPASSWORD
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. index::
 | 
					.. index::
 | 
				
			||||||
    pair: Isolation level; Constants
 | 
					    pair: Isolation level; Constants
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -63,7 +63,7 @@ from psycopg2._psycopg import (                             # noqa
 | 
				
			||||||
    string_types, binary_types, new_type, new_array_type, register_type,
 | 
					    string_types, binary_types, new_type, new_array_type, register_type,
 | 
				
			||||||
    ISQLQuote, Notify, Diagnostics, Column,
 | 
					    ISQLQuote, Notify, Diagnostics, Column,
 | 
				
			||||||
    QueryCanceledError, TransactionRollbackError,
 | 
					    QueryCanceledError, TransactionRollbackError,
 | 
				
			||||||
    set_wait_callback, get_wait_callback, )
 | 
					    set_wait_callback, get_wait_callback, encrypt_password, )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Isolation level values."""
 | 
					"""Isolation level values."""
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -407,6 +407,105 @@ psyco_libpq_version(PyObject *self)
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* encrypt_password - Prepare the encrypted password form */
 | 
				
			||||||
 | 
					#define psyco_encrypt_password_doc \
 | 
				
			||||||
 | 
					"encrypt_password(password, user, [scope], [algorithm]) -- Prepares the encrypted form of a PostgreSQL password.\n\n"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static PyObject *
 | 
				
			||||||
 | 
					psyco_encrypt_password(PyObject *self, PyObject *args, PyObject *kwargs)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    char *encrypted = NULL;
 | 
				
			||||||
 | 
					    PyObject *password = NULL, *user = NULL;
 | 
				
			||||||
 | 
					    PyObject *scope = Py_None, *algorithm = Py_None;
 | 
				
			||||||
 | 
					    PyObject *res = NULL;
 | 
				
			||||||
 | 
					    connectionObject *conn = NULL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    static char *kwlist[] = {"password", "user", "scope", "algorithm", NULL};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OO|OO", kwlist,
 | 
				
			||||||
 | 
					            &password, &user, &scope, &algorithm)) {
 | 
				
			||||||
 | 
					        return NULL;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* for ensure_bytes */
 | 
				
			||||||
 | 
					    Py_INCREF(user);
 | 
				
			||||||
 | 
					    Py_INCREF(password);
 | 
				
			||||||
 | 
					    Py_INCREF(algorithm);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (scope != Py_None) {
 | 
				
			||||||
 | 
					        if (PyObject_TypeCheck(scope, &cursorType)) {
 | 
				
			||||||
 | 
					            conn = ((cursorObject*)scope)->conn;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else if (PyObject_TypeCheck(scope, &connectionType)) {
 | 
				
			||||||
 | 
					            conn = (connectionObject*)scope;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else {
 | 
				
			||||||
 | 
					            PyErr_SetString(PyExc_TypeError,
 | 
				
			||||||
 | 
					                "the scope must be a connection or a cursor");
 | 
				
			||||||
 | 
					            goto exit;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!(user = psycopg_ensure_bytes(user))) { goto exit; }
 | 
				
			||||||
 | 
					    if (!(password = psycopg_ensure_bytes(password))) { goto exit; }
 | 
				
			||||||
 | 
					    if (algorithm != Py_None) {
 | 
				
			||||||
 | 
					        if (!(algorithm = psycopg_ensure_bytes(algorithm))) {
 | 
				
			||||||
 | 
					            goto exit;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* If we have to encrypt md5 we can use the libpq < 10 API */
 | 
				
			||||||
 | 
					    if (algorithm != Py_None &&
 | 
				
			||||||
 | 
					            strcmp(Bytes_AS_STRING(algorithm), "md5") == 0) {
 | 
				
			||||||
 | 
					        encrypted = PQencryptPassword(
 | 
				
			||||||
 | 
					            Bytes_AS_STRING(password), Bytes_AS_STRING(user));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* If the algorithm is not md5 we have to use the API available from
 | 
				
			||||||
 | 
					     * libpq 10. */
 | 
				
			||||||
 | 
					    else {
 | 
				
			||||||
 | 
					#if PG_VERSION_NUM >= 100000
 | 
				
			||||||
 | 
					        if (!conn) {
 | 
				
			||||||
 | 
					            PyErr_SetString(ProgrammingError,
 | 
				
			||||||
 | 
					                "password encryption (other than 'md5' algorithm)"
 | 
				
			||||||
 | 
					                " requires a connection or cursor");
 | 
				
			||||||
 | 
					            goto exit;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /* TODO: algo = None will block: forbid on async/green conn? */
 | 
				
			||||||
 | 
					        encrypted = PQencryptPasswordConn(conn->pgconn,
 | 
				
			||||||
 | 
					            Bytes_AS_STRING(password), Bytes_AS_STRING(user),
 | 
				
			||||||
 | 
					            algorithm != Py_None ? Bytes_AS_STRING(algorithm) : NULL);
 | 
				
			||||||
 | 
					#else
 | 
				
			||||||
 | 
					        PyErr_SetString(NotSupportedError,
 | 
				
			||||||
 | 
					            "password encryption (other than 'md5' algorithm)"
 | 
				
			||||||
 | 
					            " requires libpq 10");
 | 
				
			||||||
 | 
					        goto exit;
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (encrypted) {
 | 
				
			||||||
 | 
					        res = Text_FromUTF8(encrypted);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    else {
 | 
				
			||||||
 | 
					        const char *msg = PQerrorMessage(conn->pgconn);
 | 
				
			||||||
 | 
					        PyErr_Format(ProgrammingError,
 | 
				
			||||||
 | 
					            "password encryption failed: %s", msg ? msg : "no reason given");
 | 
				
			||||||
 | 
					        goto exit;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					exit:
 | 
				
			||||||
 | 
					    if (encrypted) {
 | 
				
			||||||
 | 
					        PQfreemem(encrypted);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    Py_XDECREF(user);
 | 
				
			||||||
 | 
					    Py_XDECREF(password);
 | 
				
			||||||
 | 
					    Py_XDECREF(algorithm);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return res;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* psyco_encodings_fill
 | 
					/* psyco_encodings_fill
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   Fill the module's postgresql<->python encoding table */
 | 
					   Fill the module's postgresql<->python encoding table */
 | 
				
			||||||
| 
						 | 
					@ -856,6 +955,8 @@ static PyMethodDef psycopgMethods[] = {
 | 
				
			||||||
     METH_O, psyco_set_wait_callback_doc},
 | 
					     METH_O, psyco_set_wait_callback_doc},
 | 
				
			||||||
    {"get_wait_callback",  (PyCFunction)psyco_get_wait_callback,
 | 
					    {"get_wait_callback",  (PyCFunction)psyco_get_wait_callback,
 | 
				
			||||||
     METH_NOARGS, psyco_get_wait_callback_doc},
 | 
					     METH_NOARGS, psyco_get_wait_callback_doc},
 | 
				
			||||||
 | 
					    {"encrypt_password", (PyCFunction)psyco_encrypt_password,
 | 
				
			||||||
 | 
					     METH_VARARGS|METH_KEYWORDS, psyco_encrypt_password_doc},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {NULL, NULL, 0, NULL}        /* Sentinel */
 | 
					    {NULL, NULL, 0, NULL}        /* Sentinel */
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,7 +37,9 @@ from psycopg2 import extensions as ext
 | 
				
			||||||
from .testutils import (
 | 
					from .testutils import (
 | 
				
			||||||
    unittest, decorate_all_tests, skip_if_no_superuser,
 | 
					    unittest, decorate_all_tests, skip_if_no_superuser,
 | 
				
			||||||
    skip_before_postgres, skip_after_postgres, skip_before_libpq,
 | 
					    skip_before_postgres, skip_after_postgres, skip_before_libpq,
 | 
				
			||||||
    ConnectingTestCase, skip_if_tpc_disabled, skip_if_windows, slow)
 | 
					    ConnectingTestCase, skip_if_tpc_disabled, skip_if_windows, slow,
 | 
				
			||||||
 | 
					    libpq_version
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .testconfig import dsn, dbname
 | 
					from .testconfig import dsn, dbname
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1406,6 +1408,92 @@ class TransactionControlTests(ConnectingTestCase):
 | 
				
			||||||
        self.assertEqual(cur.fetchone()[0], 'on')
 | 
					        self.assertEqual(cur.fetchone()[0], 'on')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestEncryptPassword(ConnectingTestCase):
 | 
				
			||||||
 | 
					    @skip_before_postgres(10)
 | 
				
			||||||
 | 
					    def test_encrypt_password_post_9_6(self):
 | 
				
			||||||
 | 
					        cur = self.conn.cursor()
 | 
				
			||||||
 | 
					        cur.execute("SHOW password_encryption;")
 | 
				
			||||||
 | 
					        server_encryption_algorithm = cur.fetchone()[0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # MD5 algorithm
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            ext.encrypt_password('psycopg2', 'ashesh', self.conn, 'md5'),
 | 
				
			||||||
 | 
					            'md594839d658c28a357126f105b9cb14cfc'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # keywords
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            ext.encrypt_password(
 | 
				
			||||||
 | 
					                password='psycopg2', user='ashesh',
 | 
				
			||||||
 | 
					                scope=self.conn, algorithm='md5'),
 | 
				
			||||||
 | 
					            'md594839d658c28a357126f105b9cb14cfc'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        if libpq_version() < 100000:
 | 
				
			||||||
 | 
					            self.assertRaises(
 | 
				
			||||||
 | 
					                psycopg2.NotSupportedError,
 | 
				
			||||||
 | 
					                ext.encrypt_password, 'psycopg2', 'ashesh', self.conn,
 | 
				
			||||||
 | 
					                'scram-sha-256'
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            enc_password = ext.encrypt_password(
 | 
				
			||||||
 | 
					                'psycopg2', 'ashesh', self.conn
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            if server_encryption_algorithm == 'md5':
 | 
				
			||||||
 | 
					                self.assertEqual(
 | 
				
			||||||
 | 
					                    enc_password, 'md594839d658c28a357126f105b9cb14cfc'
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            elif server_encryption_algorithm == 'scram-sha-256':
 | 
				
			||||||
 | 
					                self.assertEqual(enc_password[:14], 'SCRAM-SHA-256$')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            self.assertEqual(
 | 
				
			||||||
 | 
					                ext.encrypt_password(
 | 
				
			||||||
 | 
					                    'psycopg2', 'ashesh', self.conn, 'scram-sha-256'
 | 
				
			||||||
 | 
					                )[:14], 'SCRAM-SHA-256$'
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertRaises(psycopg2.ProgrammingError,
 | 
				
			||||||
 | 
					            ext.encrypt_password, 'psycopg2', 'ashesh', self.conn, 'abc')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @skip_after_postgres(10)
 | 
				
			||||||
 | 
					    def test_encrypt_password_pre_10(self):
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            ext.encrypt_password('psycopg2', 'ashesh', self.conn),
 | 
				
			||||||
 | 
					            'md594839d658c28a357126f105b9cb14cfc'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertRaises(psycopg2.ProgrammingError,
 | 
				
			||||||
 | 
					            ext.encrypt_password, 'psycopg2', 'ashesh', self.conn, 'abc')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_encrypt_md5(self):
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            ext.encrypt_password('psycopg2', 'ashesh', algorithm='md5'),
 | 
				
			||||||
 | 
					            'md594839d658c28a357126f105b9cb14cfc'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_encrypt_scram(self):
 | 
				
			||||||
 | 
					        if libpq_version() >= 100000:
 | 
				
			||||||
 | 
					            self.assert_(
 | 
				
			||||||
 | 
					                ext.encrypt_password(
 | 
				
			||||||
 | 
					                    'psycopg2', 'ashesh', self.conn, 'scram-sha-256')
 | 
				
			||||||
 | 
					                .startswith('SCRAM-SHA-256$'))
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            self.assertRaises(psycopg2.NotSupportedError,
 | 
				
			||||||
 | 
					                ext.encrypt_password,
 | 
				
			||||||
 | 
					                password='psycopg2', user='ashesh',
 | 
				
			||||||
 | 
					                scope=self.conn, algorithm='scram-sha-256')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_bad_types(self):
 | 
				
			||||||
 | 
					        self.assertRaises(TypeError, ext.encrypt_password)
 | 
				
			||||||
 | 
					        self.assertRaises(TypeError, ext.encrypt_password,
 | 
				
			||||||
 | 
					            'password', 42, self.conn, 'md5')
 | 
				
			||||||
 | 
					        self.assertRaises(TypeError, ext.encrypt_password,
 | 
				
			||||||
 | 
					            42, 'user', self.conn, 'md5')
 | 
				
			||||||
 | 
					        self.assertRaises(TypeError, ext.encrypt_password,
 | 
				
			||||||
 | 
					            42, 'user', 'wat', 'abc')
 | 
				
			||||||
 | 
					        self.assertRaises(TypeError, ext.encrypt_password,
 | 
				
			||||||
 | 
					            'password', 'user', 'wat', 42)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AutocommitTests(ConnectingTestCase):
 | 
					class AutocommitTests(ConnectingTestCase):
 | 
				
			||||||
    def test_closed(self):
 | 
					    def test_closed(self):
 | 
				
			||||||
        self.conn.close()
 | 
					        self.conn.close()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user