diff --git a/internal/pgbouncer/postgres.go b/internal/pgbouncer/postgres.go index cbc2e29916..b94783804a 100644 --- a/internal/pgbouncer/postgres.go +++ b/internal/pgbouncer/postgres.go @@ -41,14 +41,14 @@ func sqlAuthenticationQuery(sqlFunctionName string) string { // No replicators. `NOT pg_authid.rolreplication`, // Not the PgBouncer role itself. - `pg_authid.rolname <> ` + util.SQLQuoteLiteral(postgresqlUser), + `pg_authid.rolname <> ` + postgres.QuoteLiteral(postgresqlUser), // Those without a password expiration or an expiration in the future. `(pg_authid.rolvaliduntil IS NULL OR pg_authid.rolvaliduntil >= CURRENT_TIMESTAMP)`, }, "\n AND ") return strings.TrimSpace(` CREATE OR REPLACE FUNCTION ` + sqlFunctionName + `(username TEXT) -RETURNS TABLE(username TEXT, password TEXT) AS ` + util.SQLQuoteLiteral(` +RETURNS TABLE(username TEXT, password TEXT) AS ` + postgres.QuoteLiteral(` SELECT rolname::TEXT, rolpassword::TEXT FROM pg_catalog.pg_authid WHERE pg_authid.rolname = $1 diff --git a/internal/pgbouncer/postgres_test.go b/internal/pgbouncer/postgres_test.go index f2ce419753..3a9cf5790c 100644 --- a/internal/pgbouncer/postgres_test.go +++ b/internal/pgbouncer/postgres_test.go @@ -19,14 +19,14 @@ import ( func TestSQLAuthenticationQuery(t *testing.T) { assert.Equal(t, sqlAuthenticationQuery("some.fn_name"), `CREATE OR REPLACE FUNCTION some.fn_name(username TEXT) -RETURNS TABLE(username TEXT, password TEXT) AS ' +RETURNS TABLE(username TEXT, password TEXT) AS E' SELECT rolname::TEXT, rolpassword::TEXT FROM pg_catalog.pg_authid WHERE pg_authid.rolname = $1 AND pg_authid.rolcanlogin AND NOT pg_authid.rolsuper AND NOT pg_authid.rolreplication - AND pg_authid.rolname <> ''_crunchypgbouncer'' + AND pg_authid.rolname <> E''_crunchypgbouncer'' AND (pg_authid.rolvaliduntil IS NULL OR pg_authid.rolvaliduntil >= CURRENT_TIMESTAMP)' LANGUAGE SQL STABLE SECURITY DEFINER;`) } @@ -150,14 +150,14 @@ REVOKE ALL PRIVILEGES GRANT USAGE ON SCHEMA :"namespace" TO :"username"; CREATE OR REPLACE FUNCTION :"namespace".get_auth(username TEXT) -RETURNS TABLE(username TEXT, password TEXT) AS ' +RETURNS TABLE(username TEXT, password TEXT) AS E' SELECT rolname::TEXT, rolpassword::TEXT FROM pg_catalog.pg_authid WHERE pg_authid.rolname = $1 AND pg_authid.rolcanlogin AND NOT pg_authid.rolsuper AND NOT pg_authid.rolreplication - AND pg_authid.rolname <> ''_crunchypgbouncer'' + AND pg_authid.rolname <> E''_crunchypgbouncer'' AND (pg_authid.rolvaliduntil IS NULL OR pg_authid.rolvaliduntil >= CURRENT_TIMESTAMP)' LANGUAGE SQL STABLE SECURITY DEFINER; REVOKE ALL PRIVILEGES diff --git a/internal/postgres/sql.go b/internal/postgres/sql.go new file mode 100644 index 0000000000..8bef9aaaa6 --- /dev/null +++ b/internal/postgres/sql.go @@ -0,0 +1,22 @@ +// Copyright 2021 - 2024 Crunchy Data Solutions, Inc. +// +// SPDX-License-Identifier: Apache-2.0 + +package postgres + +import "strings" + +// escapeLiteral is called by QuoteLiteral to add backslashes before special +// characters of the "escape" string syntax. Double quote marks to escape them +// regardless of the "backslash_quote" parameter. +var escapeLiteral = strings.NewReplacer(`'`, `''`, `\`, `\\`).Replace + +// QuoteLiteral escapes v so it can be safely used as a literal (or constant) +// in an SQL statement. +func QuoteLiteral(v string) string { + // Use the "escape" syntax to ensure that backslashes behave consistently regardless + // of the "standard_conforming_strings" parameter. Include a space before so + // the "E" cannot change the meaning of an adjacent SQL keyword or identifier. + // - https://www.postgresql.org/docs/current/sql-syntax-lexical.html + return ` E'` + escapeLiteral(v) + `'` +} diff --git a/internal/postgres/sql_test.go b/internal/postgres/sql_test.go new file mode 100644 index 0000000000..fdca26760c --- /dev/null +++ b/internal/postgres/sql_test.go @@ -0,0 +1,16 @@ +// Copyright 2021 - 2024 Crunchy Data Solutions, Inc. +// +// SPDX-License-Identifier: Apache-2.0 + +package postgres + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestQuoteLiteral(t *testing.T) { + assert.Equal(t, QuoteLiteral(``), ` E''`) + assert.Equal(t, QuoteLiteral(`ab"cd\ef'gh`), ` E'ab"cd\\ef''gh'`) +} diff --git a/internal/util/util.go b/internal/util/util.go deleted file mode 100644 index 72634ebbc6..0000000000 --- a/internal/util/util.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2017 - 2024 Crunchy Data Solutions, Inc. -// -// SPDX-License-Identifier: Apache-2.0 - -package util - -import ( - "strings" -) - -// SQLQuoteIdentifier quotes an "identifier" (e.g. a table or a column name) to -// be used as part of an SQL statement. -// -// Any double quotes in name will be escaped. The quoted identifier will be -// case-sensitive when used in a query. If the input string contains a zero -// byte, the result will be truncated immediately before it. -// -// Implementation borrowed from lib/pq: https://github.com/lib/pq which is -// licensed under the MIT License -func SQLQuoteIdentifier(identifier string) string { - end := strings.IndexRune(identifier, 0) - - if end > -1 { - identifier = identifier[:end] - } - - return `"` + strings.Replace(identifier, `"`, `""`, -1) + `"` -} - -// SQLQuoteLiteral quotes a 'literal' (e.g. a parameter, often used to pass literal -// to DDL and other statements that do not accept parameters) to be used as part -// of an SQL statement. -// -// Any single quotes in name will be escaped. Any backslashes (i.e. "\") will be -// replaced by two backslashes (i.e. "\\") and the C-style escape identifier -// that PostgreSQL provides ('E') will be prepended to the string. -// -// Implementation borrowed from lib/pq: https://github.com/lib/pq which is -// licensed under the MIT License. Curiously, @jkatz and @cbandy were the ones -// who worked on the patch to add this, prior to being at Crunchy Data -func SQLQuoteLiteral(literal string) string { - // This follows the PostgreSQL internal algorithm for handling quoted literals - // from libpq, which can be found in the "PQEscapeStringInternal" function, - // which is found in the libpq/fe-exec.c source file: - // https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/interfaces/libpq/fe-exec.c - // - // substitute any single-quotes (') with two single-quotes ('') - literal = strings.Replace(literal, `'`, `''`, -1) - // determine if the string has any backslashes (\) in it. - // if it does, replace any backslashes (\) with two backslashes (\\) - // then, we need to wrap the entire string with a PostgreSQL - // C-style escape. Per how "PQEscapeStringInternal" handles this case, we - // also add a space before the "E" - if strings.Contains(literal, `\`) { - literal = strings.Replace(literal, `\`, `\\`, -1) - literal = ` E'` + literal + `'` - } else { - // otherwise, we can just wrap the literal with a pair of single quotes - literal = `'` + literal + `'` - } - return literal -}