diff --git a/README.md b/README.md index eb8d80301c..536f3bcc7b 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ In addition to the standard connection parameters the driver supports a number o | adaptiveFetchMaximum | Integer | -1 | Specifies maximum number of rows, which can be calculated by adaptiveFetch. Number of rows used by adaptiveFetch cannot go above this value. Any negative number set as adaptiveFetchMaximum is used by adaptiveFetch as infinity number of rows. | localSocketAddress | String | null | Hostname or IP address given to explicitly configure the interface that the driver will bind the client side of the TCP/IP connection to when connecting. | quoteReturningIdentifiers | Boolean | true | By default we double quote returning identifiers. Some ORM's already quote them. Switch allows them to turn this off +| authenticationPluginClassName | STRING | null | fully qualified class name of the class implementing the AuthenticationPlugin interface. If this is null, the default PasswordAuthentication plugin will be used ## Contributing For information on how to contribute to the project see the [Contributing Guidelines](CONTRIBUTING.md) diff --git a/docs/documentation/head/connect.md b/docs/documentation/head/connect.md index cc47b266d0..9addbae24f 100644 --- a/docs/documentation/head/connect.md +++ b/docs/documentation/head/connect.md @@ -340,28 +340,28 @@ Connection conn = DriverManager.getConnection(url); * **tcpKeepAlive** = boolean - Enable or disable TCP keep-alive probe. The default is `false`. + Enable or disable TCP keep-alive probe. The default is `false`. * **unknownLength** = int - Certain postgresql types such as `TEXT` do not have a well defined length. - When returning meta-data about these types through functions like - `ResultSetMetaData.getColumnDisplaySize` and `ResultSetMetaData.getPrecision` - we must provide a value and various client tools have different ideas - about what they would like to see. This parameter specifies the length - to return for types of unknown length. + Certain postgresql types such as `TEXT` do not have a well defined length. + When returning meta-data about these types through functions like + `ResultSetMetaData.getColumnDisplaySize` and `ResultSetMetaData.getPrecision` + we must provide a value and various client tools have different ideas + about what they would like to see. This parameter specifies the length + to return for types of unknown length. * **stringtype** = String - Specify the type to use when binding `PreparedStatement` parameters set - via `setString()`. If `stringtype` is set to `VARCHAR` (the default), such - parameters will be sent to the server as varchar parameters. If `stringtype` - is set to `unspecified`, parameters will be sent to the server as untyped - values, and the server will attempt to infer an appropriate type. This - is useful if you have an existing application that uses `setString()` to - set parameters that are actually some other type, such as integers, and - you are unable to change the application to use an appropriate method - such as `setInt()`. + Specify the type to use when binding `PreparedStatement` parameters set + via `setString()`. If `stringtype` is set to `VARCHAR` (the default), such + parameters will be sent to the server as varchar parameters. If `stringtype` + is set to `unspecified`, parameters will be sent to the server as untyped + values, and the server will attempt to infer an appropriate type. This + is useful if you have an existing application that uses `setString()` to + set parameters that are actually some other type, such as integers, and + you are unable to change the application to use an appropriate method + such as `setInt()`. * **ApplicationName** = String @@ -371,21 +371,21 @@ Connection conn = DriverManager.getConnection(url); * **kerberosServerName** = String - The Kerberos service name to use when authenticating with GSSAPI. This - is equivalent to libpq's PGKRBSRVNAME environment variable and defaults - to "postgres". + The Kerberos service name to use when authenticating with GSSAPI. This + is equivalent to libpq's PGKRBSRVNAME environment variable and defaults + to "postgres". * **jaasApplicationName** = String - Specifies the name of the JAAS system or application login configuration. + Specifies the name of the JAAS system or application login configuration. * **jaasLogin** = boolean - Specifies whether to perform a JAAS login before authenticating with GSSAPI. - If set to `true` (the default), the driver will attempt to obtain GSS credentials - using the configured JAAS login module(s) (e.g. `Krb5LoginModule`) before - authenticating. To skip the JAAS login, for example if the native GSS - implementation is being used to obtain credentials, set this to `false`. + Specifies whether to perform a JAAS login before authenticating with GSSAPI. + If set to `true` (the default), the driver will attempt to obtain GSS credentials + using the configured JAAS login module(s) (e.g. `Krb5LoginModule`) before + authenticating. To skip the JAAS login, for example if the native GSS + implementation is being used to obtain credentials, set this to `false`. * **gssEncMode** = String @@ -400,137 +400,137 @@ Connection conn = DriverManager.getConnection(url); * **gsslib** = String - Force either SSPI (Windows transparent single-sign-on) or GSSAPI (Kerberos, via JSSE) - to be used when the server requests Kerberos or SSPI authentication. - Permissible values are auto (default, see below), sspi (force SSPI) or gssapi (force GSSAPI-JSSE). + Force either SSPI (Windows transparent single-sign-on) or GSSAPI (Kerberos, via JSSE) + to be used when the server requests Kerberos or SSPI authentication. + Permissible values are auto (default, see below), sspi (force SSPI) or gssapi (force GSSAPI-JSSE). - If this parameter is auto, SSPI is attempted if the server requests SSPI authentication, - the JDBC client is running on Windows, and the Waffle libraries required - for SSPI are on the CLASSPATH. Otherwise Kerberos/GSSAPI via JSSE is used. - Note that this behaviour does not exactly match that of libpq, which uses - Windows' SSPI libraries for Kerberos (GSSAPI) requests by default when on Windows. + If this parameter is auto, SSPI is attempted if the server requests SSPI authentication, + the JDBC client is running on Windows, and the Waffle libraries required + for SSPI are on the CLASSPATH. Otherwise Kerberos/GSSAPI via JSSE is used. + Note that this behaviour does not exactly match that of libpq, which uses + Windows' SSPI libraries for Kerberos (GSSAPI) requests by default when on Windows. - gssapi mode forces JSSE's GSSAPI to be used even if SSPI is available, matching the pre-9.4 behaviour. + gssapi mode forces JSSE's GSSAPI to be used even if SSPI is available, matching the pre-9.4 behaviour. - On non-Windows platforms or where SSPI is unavailable, forcing sspi mode will fail with a PSQLException. + On non-Windows platforms or where SSPI is unavailable, forcing sspi mode will fail with a PSQLException. To use SSPI with PgJDBC you must ensure that [the `waffle-jna` library](https://mvnrepository.com/artifact/com.github.waffle/waffle-jna/) - and its dependencies are present on the `CLASSPATH`. PgJDBC does *not* + and its dependencies are present on the `CLASSPATH`. PgJDBC does *not* bundle `waffle-jna` in the PgJDBC jar. - Since: 9.4 + Since: 9.4 * **sspiServiceClass** = String - Specifies the name of the Windows SSPI service class that forms the service - class part of the SPN. The default, POSTGRES, is almost always correct. + Specifies the name of the Windows SSPI service class that forms the service + class part of the SPN. The default, POSTGRES, is almost always correct. - See: SSPI authentication (Pg docs) Service Principal Names (MSDN), DsMakeSpn (MSDN) Configuring SSPI (Pg wiki). + See: SSPI authentication (Pg docs) Service Principal Names (MSDN), DsMakeSpn (MSDN) Configuring SSPI (Pg wiki). - This parameter is ignored on non-Windows platforms. + This parameter is ignored on non-Windows platforms. * **useSpnego** = boolean - Use SPNEGO in SSPI authentication requests + Use SPNEGO in SSPI authentication requests * **sendBufferSize** = int - Sets SO_SNDBUF on the connection stream + Sets SO_SNDBUF on the connection stream * **receiveBufferSize** = int - Sets SO_RCVBUF on the connection stream + Sets SO_RCVBUF on the connection stream * **readOnly** = boolean - Put the connection in read-only mode + Put the connection in read-only mode * **readOnlyMode** = String - One of 'ignore', 'transaction', or 'always'. Controls the behavior when a connection is set to read only, When set - to 'ignore' then the `readOnly` setting has no effect. When set to 'transaction' and `readOnly` is set to 'true' - and autocommit is 'false' the driver will set the transaction to readonly by sending `BEGIN READ ONLY`. When set to - 'always' and `readOnly` is set to 'true' the session will be set to READ ONLY if autoCommit is 'true'. If - autocommit is false the driver will set the transaction to read only by sending `BEGIN READ ONLY` . + One of 'ignore', 'transaction', or 'always'. Controls the behavior when a connection is set to read only, When set + to 'ignore' then the `readOnly` setting has no effect. When set to 'transaction' and `readOnly` is set to 'true' + and autocommit is 'false' the driver will set the transaction to readonly by sending `BEGIN READ ONLY`. When set to + 'always' and `readOnly` is set to 'true' the session will be set to READ ONLY if autoCommit is 'true'. If + autocommit is false the driver will set the transaction to read only by sending `BEGIN READ ONLY` . - The default the value is 'transaction' + The default the value is 'transaction' * **disableColumnSanitiser** = boolean - Setting this to true disables column name sanitiser. - The sanitiser folds columns in the resultset to lowercase. - The default is to sanitise the columns (off). + Setting this to true disables column name sanitiser. + The sanitiser folds columns in the resultset to lowercase. + The default is to sanitise the columns (off). * **assumeMinServerVersion** = String - Assume that the server is at least the given version, - thus enabling to some optimization at connection time instead of trying to be version blind. + Assume that the server is at least the given version, + thus enabling to some optimization at connection time instead of trying to be version blind. * **currentSchema** = String - Specify the schema (or several schema separated by commas) to be set in the search-path. - This schema will be used to resolve unqualified object names used in statements over this connection. + Specify the schema (or several schema separated by commas) to be set in the search-path. + This schema will be used to resolve unqualified object names used in statements over this connection. * **targetServerType** = String - Allows opening connections to only servers with required state, - the allowed values are any, primary, master, slave, secondary, preferSlave and preferSecondary. - The primary/secondary distinction is currently done by observing if the server allows writes. - The value preferSecondary tries to connect to secondary if any are available, - otherwise allows falls back to connecting also to primary. - - *N.B.* the words master and slave are being deprecated. We will silently accept them, but primary - and secondary are encouraged. + Allows opening connections to only servers with required state, + the allowed values are any, primary, master, slave, secondary, preferSlave and preferSecondary. + The primary/secondary distinction is currently done by observing if the server allows writes. + The value preferSecondary tries to connect to secondary if any are available, + otherwise allows falls back to connecting also to primary. + - *N.B.* the words master and slave are being deprecated. We will silently accept them, but primary + and secondary are encouraged. * **hostRecheckSeconds** = int - Controls how long in seconds the knowledge about a host state - is cached in JVM wide global cache. The default value is 10 seconds. + Controls how long in seconds the knowledge about a host state + is cached in JVM wide global cache. The default value is 10 seconds. * **loadBalanceHosts** = boolean - In default mode (disabled) hosts are connected in the given order. - If enabled hosts are chosen randomly from the set of suitable candidates. + In default mode (disabled) hosts are connected in the given order. + If enabled hosts are chosen randomly from the set of suitable candidates. * **socketFactory** = String - The provided value is a class name to use as the `SocketFactory` when establishing a socket connection. - This may be used to create unix sockets instead of normal sockets. The class name specified by `socketFactory` - must extend `javax.net.SocketFactory` and be available to the driver's classloader. - This class must have a zero-argument constructor, a single-argument constructor taking a String argument, or - a single-argument constructor taking a Properties argument. The Properties object will contain all the - connection parameters. The String argument will have the value of the `socketFactoryArg` connection parameter. + The provided value is a class name to use as the `SocketFactory` when establishing a socket connection. + This may be used to create unix sockets instead of normal sockets. The class name specified by `socketFactory` + must extend `javax.net.SocketFactory` and be available to the driver's classloader. + This class must have a zero-argument constructor, a single-argument constructor taking a String argument, or + a single-argument constructor taking a Properties argument. The Properties object will contain all the + connection parameters. The String argument will have the value of the `socketFactoryArg` connection parameter. * **socketFactoryArg** (deprecated) = String - This value is an optional argument to the constructor of the socket factory - class provided above. + This value is an optional argument to the constructor of the socket factory + class provided above. * **reWriteBatchedInserts** = boolean - This will change batch inserts from insert into foo (col1, col2, col3) values (1,2,3) into - insert into foo (col1, col2, col3) values (1,2,3), (4,5,6) this provides 2-3x performance improvement + This will change batch inserts from insert into foo (col1, col2, col3) values (1,2,3) into + insert into foo (col1, col2, col3) values (1,2,3), (4,5,6) this provides 2-3x performance improvement * **replication** = String - Connection parameter passed in the startup message. This parameter accepts two values; "true" - and `database`. Passing `true` tells the backend to go into walsender mode, wherein a small set - of replication commands can be issued instead of SQL statements. Only the simple query protocol - can be used in walsender mode. Passing "database" as the value instructs walsender to connect - to the database specified in the dbname parameter, which will allow the connection to be used - for logical replication from that database. + Connection parameter passed in the startup message. This parameter accepts two values; "true" + and `database`. Passing `true` tells the backend to go into walsender mode, wherein a small set + of replication commands can be issued instead of SQL statements. Only the simple query protocol + can be used in walsender mode. Passing "database" as the value instructs walsender to connect + to the database specified in the dbname parameter, which will allow the connection to be used + for logical replication from that database. - Parameter should be use together with `assumeMinServerVersion` with parameter >= 9.4 (backend >= 9.4) + Parameter should be use together with `assumeMinServerVersion` with parameter >= 9.4 (backend >= 9.4) * **escapeSyntaxCallMode** = String - Specifies how the driver transforms JDBC escape call syntax into underlying SQL, for invoking procedures or functions. - In `escapeSyntaxCallMode=select` mode (the default), the driver always uses a SELECT statement (allowing function invocation only). - In `escapeSyntaxCallMode=callIfNoReturn` mode, the driver uses a CALL statement (allowing procedure invocation) if there is no - return parameter specified, otherwise the driver uses a SELECT statement. - In `escapeSyntaxCallMode=call` mode, the driver always uses a CALL statement (allowing procedure invocation only). + Specifies how the driver transforms JDBC escape call syntax into underlying SQL, for invoking procedures or functions. + In `escapeSyntaxCallMode=select` mode (the default), the driver always uses a SELECT statement (allowing function invocation only). + In `escapeSyntaxCallMode=callIfNoReturn` mode, the driver uses a CALL statement (allowing procedure invocation) if there is no + return parameter specified, otherwise the driver uses a SELECT statement. + In `escapeSyntaxCallMode=call` mode, the driver always uses a CALL statement (allowing procedure invocation only). - The default is `select` + The default is `select` * **maxResultBuffer** = String @@ -541,7 +541,7 @@ Connection conn = DriverManager.getConnection(url); A limit during setting of property is 90% of max heap memory. All given values, which gonna be higher than limit, gonna lowered to the limit. - By default, maxResultBuffer is not set (is null), what means that reading of results gonna be performed without limits. + By default, maxResultBuffer is not set (is null), what means that reading of results gonna be performed without limits. * **adaptiveFetch** = boolean @@ -563,7 +563,7 @@ Connection conn = DriverManager.getConnection(url); * **adaptiveFetchMaximum** = int - Specifies the highest number of rows which can be calculated by `adaptiveFetch`. + Specifies the highest number of rows which can be calculated by `adaptiveFetch`. Requires `adaptiveFetch` set to true to work. By default, maximum of rows calculated by `adaptiveFetch` is -1, which is understood as infinite. @@ -571,9 +571,9 @@ Connection conn = DriverManager.getConnection(url); * **logServerErrorDetail** == boolean Whether to include server error details in exceptions and log messages (for example inlined query parameters). - Setting to false will only include minimal, not sensitive messages. + Setting to false will only include minimal, not sensitive messages. - By default this is set to true, server error details are propagated. This may include sensitive details such as query parameters. + By default this is set to true, server error details are propagated. This may include sensitive details such as query parameters. * **quoteReturningIdentifiers** == boolean @@ -582,6 +582,11 @@ Connection conn = DriverManager.getConnection(url); If we quote them, then we end up sending ""colname"" to the backend instead of "colname" which will not be found. +* **authenticationPluginClassName** == String + + Fully qualified class name of the class implementing the AuthenticationPlugin interface. + If this is null, the default PasswordAuthentication plugin will be used. + ## Unix sockets diff --git a/pgjdbc/src/main/java/org/postgresql/Driver.java b/pgjdbc/src/main/java/org/postgresql/Driver.java index 8b950a44a6..8f304e4c82 100644 --- a/pgjdbc/src/main/java/org/postgresql/Driver.java +++ b/pgjdbc/src/main/java/org/postgresql/Driver.java @@ -463,7 +463,7 @@ public Connection getResult(long timeout) throws SQLException { * @throws SQLException if the connection could not be made */ private static Connection makeConnection(String url, Properties props) throws SQLException { - return new PgConnection(hostSpecs(props), user(props), database(props), props, url); + return new PgConnection(hostSpecs(props), props, url); } /** @@ -657,8 +657,8 @@ private static HostSpec[] hostSpecs(Properties props) { /** * @return the username of the URL */ - private static String user(Properties props) { - return props.getProperty("user", ""); + private static @Nullable String user(Properties props) { + return PGProperty.USER.get(props); } /** diff --git a/pgjdbc/src/main/java/org/postgresql/PGProperty.java b/pgjdbc/src/main/java/org/postgresql/PGProperty.java index 62e21f15ee..3dc9cf856a 100644 --- a/pgjdbc/src/main/java/org/postgresql/PGProperty.java +++ b/pgjdbc/src/main/java/org/postgresql/PGProperty.java @@ -82,6 +82,16 @@ public enum PGProperty { null, "Assume the server is at least that version"), + /** + * AuthenticationPluginClass + */ + + AUTHENTICATION_PLUGIN_CLASS_NAME( + "authenticationPluginClassName", + null, + "Name of class which implements AuthenticationPlugin" + ), + /** * Specifies what the driver should do if a query fails. In {@code autosave=always} mode, JDBC driver sets a savepoint before each query, * and rolls back to that savepoint in case of failure. In {@code autosave=never} mode (default), no savepoint dance is made ever. diff --git a/pgjdbc/src/main/java/org/postgresql/core/AuthenticationPlugin.java b/pgjdbc/src/main/java/org/postgresql/core/AuthenticationPlugin.java new file mode 100644 index 0000000000..f3a0c76247 --- /dev/null +++ b/pgjdbc/src/main/java/org/postgresql/core/AuthenticationPlugin.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2021, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.core; + +import org.postgresql.util.PSQLException; + +import java.util.Properties; + +public interface AuthenticationPlugin { + + byte[] getEncodedPassword(Properties info) throws PSQLException, PSQLException; + +} diff --git a/pgjdbc/src/main/java/org/postgresql/core/AuthenticationPluginManager.java b/pgjdbc/src/main/java/org/postgresql/core/AuthenticationPluginManager.java new file mode 100644 index 0000000000..b82dadd03e --- /dev/null +++ b/pgjdbc/src/main/java/org/postgresql/core/AuthenticationPluginManager.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2021, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.core; + +import org.postgresql.PGProperty; +import org.postgresql.util.ObjectFactory; + +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class AuthenticationPluginManager { + + private static final Logger LOGGER = Logger.getLogger(AuthenticationPluginManager.class.getName()); + + public static AuthenticationPlugin getAuthenticationPlugin(Properties info) throws Exception { + String authenticationClassName = PGProperty.AUTHENTICATION_PLUGIN_CLASS_NAME.getSetString(info); + + if ( authenticationClassName == null ) { + return new PasswordAuthentication(info); + } else { + try { + return (AuthenticationPlugin) ObjectFactory.instantiate(authenticationClassName, info, false, null); + } catch (Exception ex ) { + LOGGER.log(Level.FINE, "Unable to load Authentication Plugin " + ex.toString() ); + throw ex; + } + } + } +} diff --git a/pgjdbc/src/main/java/org/postgresql/core/ConnectionFactory.java b/pgjdbc/src/main/java/org/postgresql/core/ConnectionFactory.java index ecf0665bcd..0780fbafc3 100644 --- a/pgjdbc/src/main/java/org/postgresql/core/ConnectionFactory.java +++ b/pgjdbc/src/main/java/org/postgresql/core/ConnectionFactory.java @@ -35,21 +35,19 @@ public abstract class ConnectionFactory { * * @param hostSpecs at least one host and port to connect to; multiple elements for round-robin * failover - * @param user the username to authenticate with; may not be null. - * @param database the database on the server to connect to; may not be null. * @param info extra properties controlling the connection; notably, "password" if present * supplies the password to authenticate with. * @return the new, initialized, connection * @throws SQLException if the connection could not be established. */ - public static QueryExecutor openConnection(HostSpec[] hostSpecs, String user, - String database, Properties info) throws SQLException { + public static QueryExecutor openConnection(HostSpec[] hostSpecs, + Properties info) throws SQLException { String protoName = PGProperty.PROTOCOL_VERSION.get(info); if (protoName == null || protoName.isEmpty() || "3".equals(protoName)) { ConnectionFactory connectionFactory = new ConnectionFactoryImpl(); QueryExecutor queryExecutor = connectionFactory.openConnectionImpl( - hostSpecs, user, database, info); + hostSpecs, info); if (queryExecutor != null) { return queryExecutor; } @@ -66,8 +64,6 @@ public static QueryExecutor openConnection(HostSpec[] hostSpecs, String user, * * @param hostSpecs at least one host and port to connect to; multiple elements for round-robin * failover - * @param user the username to authenticate with; may not be null. - * @param database the database on the server to connect to; may not be null. * @param info extra properties controlling the connection; notably, "password" if present * supplies the password to authenticate with. * @return the new, initialized, connection, or null if this protocol version is not @@ -75,8 +71,7 @@ public static QueryExecutor openConnection(HostSpec[] hostSpecs, String user, * @throws SQLException if the connection could not be established for a reason other than * protocol version incompatibility. */ - public abstract QueryExecutor openConnectionImpl(HostSpec[] hostSpecs, String user, - String database, Properties info) throws SQLException; + public abstract QueryExecutor openConnectionImpl(HostSpec[] hostSpecs, Properties info) throws SQLException; /** * Safely close the given stream. diff --git a/pgjdbc/src/main/java/org/postgresql/core/PasswordAuthentication.java b/pgjdbc/src/main/java/org/postgresql/core/PasswordAuthentication.java new file mode 100644 index 0000000000..81f414d065 --- /dev/null +++ b/pgjdbc/src/main/java/org/postgresql/core/PasswordAuthentication.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.core; + +import org.postgresql.PGProperty; +import org.postgresql.util.GT; +import org.postgresql.util.PSQLException; +import org.postgresql.util.PSQLState; + +import java.nio.charset.StandardCharsets; +import java.util.Properties; + +public class PasswordAuthentication implements AuthenticationPlugin { + + public PasswordAuthentication(Properties info) { + // ignore we don't need it. + } + + @Override + public byte[] getEncodedPassword(Properties info) throws PSQLException { + String password = PGProperty.PASSWORD.getSetString(info); + + if (password == null) { + throw new PSQLException( + GT.tr( + "The server requested password-based authentication, but no password was provided."), + PSQLState.CONNECTION_REJECTED); + } + + return password.getBytes(StandardCharsets.UTF_8); + } +} diff --git a/pgjdbc/src/main/java/org/postgresql/core/QueryExecutorBase.java b/pgjdbc/src/main/java/org/postgresql/core/QueryExecutorBase.java index 30ea1907d0..6ed7f768a1 100644 --- a/pgjdbc/src/main/java/org/postgresql/core/QueryExecutorBase.java +++ b/pgjdbc/src/main/java/org/postgresql/core/QueryExecutorBase.java @@ -67,11 +67,10 @@ public abstract class QueryExecutorBase implements QueryExecutor { = new TreeMap(String.CASE_INSENSITIVE_ORDER); @SuppressWarnings({"assignment.type.incompatible", "argument.type.incompatible"}) - protected QueryExecutorBase(PGStream pgStream, String user, - String database, int cancelSignalTimeout, Properties info) throws SQLException { + protected QueryExecutorBase(PGStream pgStream, int cancelSignalTimeout, Properties info) throws SQLException { this.pgStream = pgStream; - this.user = user; - this.database = database; + this.user = PGProperty.USER.get(info); + this.database = PGProperty.PG_DBNAME.get(info); this.cancelSignalTimeout = cancelSignalTimeout; this.reWriteBatchedInserts = PGProperty.REWRITE_BATCHED_INSERTS.getBoolean(info); this.columnSanitiserDisabled = PGProperty.DISABLE_COLUMN_SANITISER.getBoolean(info); diff --git a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java index 56f48f15c0..f71861efe2 100644 --- a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java +++ b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java @@ -9,6 +9,8 @@ import static org.postgresql.util.internal.Nullness.castNonNull; import org.postgresql.PGProperty; +import org.postgresql.core.AuthenticationPlugin; +import org.postgresql.core.AuthenticationPluginManager; import org.postgresql.core.ConnectionFactory; import org.postgresql.core.PGStream; import org.postgresql.core.QueryExecutor; @@ -91,11 +93,21 @@ private ISSPIClient createSSPI(PGStream pgStream, } } - private PGStream tryConnect(String user, String database, - Properties info, SocketFactory socketFactory, HostSpec hostSpec, + private PGStream tryConnect(Properties info, SocketFactory socketFactory, HostSpec hostSpec, SslMode sslMode, GSSEncMode gssEncMode) throws SQLException, IOException { + int connectTimeout = PGProperty.CONNECT_TIMEOUT.getInt(info) * 1000; + String user = PGProperty.USER.get(info); + String database = PGProperty.PG_DBNAME.get(info); + if (user == null) { + throw new PSQLException(GT.tr("User cannot be null"), + PSQLState.INVALID_NAME); + } + if (database == null) { + throw new PSQLException(GT.tr("Database cannot be null"), + PSQLState.INVALID_NAME); + } PGStream newStream = new PGStream(socketFactory, hostSpec, connectTimeout); @@ -143,7 +155,7 @@ private PGStream tryConnect(String user, String database, LOGGER.log(Level.FINE, "Send Buffer Size is {0}", newStream.getSocket().getSendBufferSize()); } - newStream = enableGSSEncrypted(newStream, gssEncMode, hostSpec.getHost(), user, info, connectTimeout); + newStream = enableGSSEncrypted(newStream, gssEncMode, hostSpec.getHost(), info, connectTimeout); // if we have a security context then gss negotiation succeeded. Do not attempt SSL negotiation if (!newStream.isGssEncrypted()) { @@ -166,8 +178,7 @@ private PGStream tryConnect(String user, String database, } @Override - public QueryExecutor openConnectionImpl(HostSpec[] hostSpecs, String user, String database, - Properties info) throws SQLException { + public QueryExecutor openConnectionImpl(HostSpec[] hostSpecs, Properties info) throws SQLException { SslMode sslMode = SslMode.of(info); GSSEncMode gssEncMode = GSSEncMode.of(info); @@ -212,7 +223,7 @@ public QueryExecutor openConnectionImpl(HostSpec[] hostSpecs, String user, Strin PGStream newStream = null; try { try { - newStream = tryConnect(user, database, info, socketFactory, hostSpec, sslMode, gssEncMode); + newStream = tryConnect(info, socketFactory, hostSpec, sslMode, gssEncMode); } catch (SQLException e) { if (sslMode == SslMode.PREFER && PSQLState.INVALID_AUTHORIZATION_SPECIFICATION.getState().equals(e.getSQLState())) { @@ -221,14 +232,13 @@ public QueryExecutor openConnectionImpl(HostSpec[] hostSpecs, String user, Strin Throwable ex = null; try { newStream = - tryConnect(user, database, info, socketFactory, hostSpec, SslMode.DISABLE,gssEncMode); + tryConnect(info, socketFactory, hostSpec, SslMode.DISABLE,gssEncMode); LOGGER.log(Level.FINE, "Downgraded to non-encrypted connection for host {0}", hostSpec); - } catch (SQLException ee) { + } catch (SQLException | IOException ee) { ex = ee; - } catch (IOException ee) { - ex = ee; // Can't use multi-catch in Java 6 :( } + if (ex != null) { log(Level.FINE, "sslMode==PREFER, however non-SSL connection failed as well", ex); // non-SSL failed as well, so re-throw original exception @@ -242,7 +252,7 @@ public QueryExecutor openConnectionImpl(HostSpec[] hostSpecs, String user, Strin Throwable ex = null; try { newStream = - tryConnect(user, database, info, socketFactory, hostSpec, SslMode.REQUIRE, gssEncMode); + tryConnect(info, socketFactory, hostSpec, SslMode.REQUIRE, gssEncMode); LOGGER.log(Level.FINE, "Upgraded to encrypted connection for host {0}", hostSpec); } catch (SQLException ee) { @@ -268,8 +278,7 @@ public QueryExecutor openConnectionImpl(HostSpec[] hostSpecs, String user, Strin // CheckerFramework can't infer newStream is non-nullable castNonNull(newStream); // Do final startup. - QueryExecutor queryExecutor = new QueryExecutorImpl(newStream, user, database, - cancelSignalTimeout, info); + QueryExecutor queryExecutor = new QueryExecutorImpl(newStream, cancelSignalTimeout, info); // Check Primary or Secondary HostStatus hostStatus = HostStatus.ConnectOK; @@ -413,7 +422,7 @@ private boolean credentialCacheExists() { return Unsafe.credentialCacheExists(); } - private PGStream enableGSSEncrypted(PGStream pgStream, GSSEncMode gssEncMode, String host, String user, Properties info, + private PGStream enableGSSEncrypted(PGStream pgStream, GSSEncMode gssEncMode, String host, Properties info, int connectTimeout) throws IOException, PSQLException { @@ -435,6 +444,11 @@ private PGStream enableGSSEncrypted(PGStream pgStream, GSSEncMode gssEncMode, St } } + String user = PGProperty.USER.get(info); + if (user == null) { + throw new PSQLException("GSSAPI encryption required but was impossible user is null", PSQLState.CONNECTION_REJECTED); + } + // attempt to acquire a GSS encrypted connection String password = PGProperty.PASSWORD.get(info); LOGGER.log(Level.FINEST, " FE=> GSSENCRequest"); @@ -595,6 +609,13 @@ private void doAuthentication(PGStream pgStream, String host, String user, Prope // Now get the response from the backend, either an error message // or an authentication request + AuthenticationPlugin authenticationPlugin; + try { + authenticationPlugin = AuthenticationPluginManager.getAuthenticationPlugin(info); + } catch ( Exception ex ) { + throw new PSQLException(ex.getMessage(), PSQLState.UNEXPECTED_ERROR); + } + String password = PGProperty.PASSWORD.get(info); /* SSPI negotiation state, if used */ @@ -665,14 +686,7 @@ private void doAuthentication(PGStream pgStream, String host, String user, Prope LOGGER.log(Level.FINEST, "<=BE AuthenticationReqPassword"); LOGGER.log(Level.FINEST, " FE=> Password(password=)"); - if (password == null) { - throw new PSQLException( - GT.tr( - "The server requested password-based authentication, but no password was provided."), - PSQLState.CONNECTION_REJECTED); - } - - byte[] encodedPassword = password.getBytes(StandardCharsets.UTF_8); + byte[] encodedPassword = authenticationPlugin.getEncodedPassword(info); pgStream.sendChar('p'); pgStream.sendInteger4(4 + encodedPassword.length + 1); diff --git a/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java b/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java index bb45f5aac7..05a3826a20 100644 --- a/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java +++ b/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java @@ -141,9 +141,9 @@ public class QueryExecutorImpl extends QueryExecutorBase { @SuppressWarnings({"assignment.type.incompatible", "argument.type.incompatible", "method.invocation.invalid"}) - public QueryExecutorImpl(PGStream pgStream, String user, String database, + public QueryExecutorImpl(PGStream pgStream, int cancelSignalTimeout, Properties info) throws SQLException, IOException { - super(pgStream, user, database, cancelSignalTimeout, info); + super(pgStream, cancelSignalTimeout, info); long maxResultBuffer = pgStream.getMaxResultBuffer(); this.adaptiveFetchCache = new AdaptiveFetchCache(maxResultBuffer, info); diff --git a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java index 0b8df59396..15f5f1ec9b 100644 --- a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java +++ b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java @@ -1305,6 +1305,26 @@ public void setURL(String url) { setUrl(url); } + /** + * + * @return the class name to use for the Authentication Plugin. + * This can be null in which case the default password authentication plugin will be used + */ + public @Nullable String getAuthenticationPluginClassName() { + return PGProperty.AUTHENTICATION_PLUGIN_CLASS_NAME.get(properties); + } + + /** + * + * @param className name of a class which implements {@link org.postgresql.core.AuthenticationPlugin} + * This class will be used to get the encoded bytes to be sent to the server as the + * password to authenticate the user. + * + */ + public void setAuthenticationPluginClassName(String className) { + PGProperty.AUTHENTICATION_PLUGIN_CLASS_NAME.set(properties, className); + } + public @Nullable String getProperty(String name) throws SQLException { PGProperty pgProperty = PGProperty.forName(name); if (pgProperty != null) { diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java index 2b98f5e1a2..65fce83800 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java @@ -203,8 +203,6 @@ public void setFlushCacheOnDeallocate(boolean flushCacheOnDeallocate) { // @SuppressWarnings({"method.invocation.invalid", "argument.type.incompatible"}) public PgConnection(HostSpec[] hostSpecs, - String user, - String database, Properties info, String url) throws SQLException { // Print out the driver version number @@ -222,7 +220,7 @@ public PgConnection(HostSpec[] hostSpecs, } // Now make the initial connection and set up local state - this.queryExecutor = ConnectionFactory.openConnection(hostSpecs, user, database, info); + this.queryExecutor = ConnectionFactory.openConnection(hostSpecs, info); // WARNING for unsupported servers (8.1 and lower are not supported) if (LOGGER.isLoggable(Level.WARNING) && !haveMinimumServerVersion(ServerVersion.v8_2)) {