diff --git a/internal/stmtcache/lru.go b/internal/stmtcache/lru.go index 938ddee4..a3378c86 100644 --- a/internal/stmtcache/lru.go +++ b/internal/stmtcache/lru.go @@ -102,10 +102,14 @@ func (c *LRU) StatementErrored(sql string, err error) { return } - isInvalidCachedPlanError := pgErr.Severity == "ERROR" && - pgErr.Code == "0A000" && - pgErr.Message == "cached plan must not change result type" - if isInvalidCachedPlanError { + // https://github.com/jackc/pgx/issues/1162 + // + // We used to look for the message "cached plan must not change result type". However, that message can be localized. + // Unfortunately, error code "0A000" - "FEATURE NOT SUPPORTED" is used for many different errors and the only way to + // tell the difference is by the message. But all that happens is we clear a statement that we otherwise wouldn't + // have so it should be safe. + possibleInvalidCachedPlanError := pgErr.Code == "0A000" + if possibleInvalidCachedPlanError { c.stmtsToClear = append(c.stmtsToClear, sql) } } diff --git a/pgconn/config.go b/pgconn/config.go index e99dabfb..2236bbe6 100644 --- a/pgconn/config.go +++ b/pgconn/config.go @@ -175,8 +175,6 @@ func NetworkAddress(host string, port uint16) (network, address string) { // // Other known differences with libpq: // -// If a host name resolves into multiple addresses, libpq will try all addresses. pgconn will only try the first. -// // When multiple hosts are specified, libpq allows them to have different passwords set via the .pgpass file. pgconn // does not. // @@ -253,6 +251,8 @@ func ParseConfig(connString string) (*Config, error) { "sslkey": {}, "sslcert": {}, "sslrootcert": {}, + "krbspn": {}, + "krbsrvname": {}, "target_session_attrs": {}, "service": {}, "servicefile": {}, diff --git a/pgconn/krb5.go b/pgconn/krb5.go new file mode 100644 index 00000000..bffbc981 --- /dev/null +++ b/pgconn/krb5.go @@ -0,0 +1,95 @@ +package pgconn + +import ( + "errors" + + "github.com/jackc/pgx/v5/pgproto3" +) + +// NewGSSFunc creates a GSS authentication provider, for use with +// RegisterGSSProvider. +type NewGSSFunc func() (GSS, error) + +var newGSS NewGSSFunc + +// RegisterGSSProvider registers a GSS authentication provider. For example, if +// you need to use Kerberos to authenticate with your server, add this to your +// main package: +// +// import "github.com/otan/gopgkrb5" +// +// func init() { +// pgconn.RegisterGSSProvider(func() (pgconn.GSS, error) { return gopgkrb5.NewGSS() }) +// } +func RegisterGSSProvider(newGSSArg NewGSSFunc) { + newGSS = newGSSArg +} + +// GSS provides GSSAPI authentication (e.g., Kerberos). +type GSS interface { + GetInitToken(host string, service string) ([]byte, error) + GetInitTokenFromSPN(spn string) ([]byte, error) + Continue(inToken []byte) (done bool, outToken []byte, err error) +} + +func (c *PgConn) gssAuth() error { + if newGSS == nil { + return errors.New("kerberos error: no GSSAPI provider registered, see https://github.com/otan/gopgkrb5") + } + cli, err := newGSS() + if err != nil { + return err + } + + var nextData []byte + if spn, ok := c.config.RuntimeParams["krbspn"]; ok { + // Use the supplied SPN if provided. + nextData, err = cli.GetInitTokenFromSPN(spn) + } else { + // Allow the kerberos service name to be overridden + service := "postgres" + if val, ok := c.config.RuntimeParams["krbsrvname"]; ok { + service = val + } + nextData, err = cli.GetInitToken(c.config.Host, service) + } + if err != nil { + return err + } + + for { + gssResponse := &pgproto3.GSSResponse{ + Data: nextData, + } + _, err = c.conn.Write(gssResponse.Encode(nil)) + if err != nil { + return err + } + resp, err := c.rxGSSContinue() + if err != nil { + return err + } + var done bool + done, nextData, err = cli.Continue(resp.Data) + if err != nil { + return err + } + if done { + break + } + } + return nil +} + +func (c *PgConn) rxGSSContinue() (*pgproto3.AuthenticationGSSContinue, error) { + msg, err := c.receiveMessage() + if err != nil { + return nil, err + } + gssContinue, ok := msg.(*pgproto3.AuthenticationGSSContinue) + if ok { + return gssContinue, nil + } + + return nil, errors.New("expected AuthenticationGSSContinue message but received unexpected message") +} diff --git a/pgconn/pgconn.go b/pgconn/pgconn.go index 9c5e8e79..a23b1daf 100644 --- a/pgconn/pgconn.go +++ b/pgconn/pgconn.go @@ -99,7 +99,7 @@ type PgConn struct { } // Connect establishes a connection to a PostgreSQL server using the environment and connString (in URL or DSN format) -// to provide configuration. See documention for ParseConfig for details. ctx can be used to cancel a connect attempt. +// to provide configuration. See documentation for ParseConfig for details. ctx can be used to cancel a connect attempt. func Connect(ctx context.Context, connString string) (*PgConn, error) { config, err := ParseConfig(connString) if err != nil { @@ -154,9 +154,14 @@ func ConnectConfig(ctx context.Context, config *Config) (pgConn *PgConn, err err break } else if pgerr, ok := err.(*PgError); ok { err = &connectError{config: config, msg: "server error", err: pgerr} - ERRCODE_INVALID_PASSWORD := "28P01" // worng password - ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION := "28000" // db does not exist - if pgerr.Code == ERRCODE_INVALID_PASSWORD || pgerr.Code == ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION { + const ERRCODE_INVALID_PASSWORD = "28P01" // wrong password + const ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION = "28000" // wrong password or bad pg_hba.conf settings + const ERRCODE_INVALID_CATALOG_NAME = "3D000" // db does not exist + const ERRCODE_INSUFFICIENT_PRIVILEGE = "42501" // missing connect privilege + if pgerr.Code == ERRCODE_INVALID_PASSWORD || + pgerr.Code == ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION || + pgerr.Code == ERRCODE_INVALID_CATALOG_NAME || + pgerr.Code == ERRCODE_INSUFFICIENT_PRIVILEGE { break } } @@ -317,7 +322,12 @@ func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig pgConn.conn.Close() return nil, &connectError{config: config, msg: "failed SASL auth", err: err} } - + case *pgproto3.AuthenticationGSS: + err = pgConn.gssAuth() + if err != nil { + pgConn.conn.Close() + return nil, &connectError{config: config, msg: "failed GSS auth", err: err} + } case *pgproto3.ReadyForQuery: pgConn.status = connStatusIdle if config.ValidateConnect != nil {