storage: fix postgres timezone handling

Dex's Postgres client currently uses the `timestamp` datatype for
storing times. This lops of timezones with no conversion, causing
times to lose locality information.

We could convert all times to UTC before storing them, but this is
a backward incompatible change for upgrades, since the new version
of dex would still be reading times from the database with no
locality.

Because of this intrinsic issue that current Postgres users don't
save any timezone data, we chose to treat any existing installation
as corrupted and change the datatype used for times to `timestamptz`.
This is a breaking change, but it seems hard to offer an
alternative that's both correct and backward compatible.

Additionally, an internal flag has been added to SQL flavors,
`supportsTimezones`. This allows us to handle SQLite3, which doesn't
support timezones, while still storing timezones in other flavors.
Flavors that don't support timezones are explicitly converted to
UTC.
This commit is contained in:
Eric Chiang
2016-12-16 11:03:36 -08:00
parent dd3133072c
commit fd20b213bb
4 changed files with 121 additions and 31 deletions

View File

@@ -9,7 +9,7 @@ func (c *conn) migrate() (int, error) {
_, err := c.Exec(`
create table if not exists migrations (
num integer not null,
at timestamp not null
at timestamptz not null
);
`)
if err != nil {
@@ -100,7 +100,7 @@ var migrations = []migration{
connector_id text not null,
connector_data bytea,
expiry timestamp not null
expiry timestamptz not null
);
create table auth_code (
@@ -119,7 +119,7 @@ var migrations = []migration{
connector_id text not null,
connector_data bytea,
expiry timestamp not null
expiry timestamptz not null
);
create table refresh_token (
@@ -151,7 +151,7 @@ var migrations = []migration{
verification_keys bytea not null, -- JSON array
signing_key bytea not null, -- JSON object
signing_key_pub bytea not null, -- JSON object
next_rotation timestamp not null
next_rotation timestamptz not null
);
`,
},

View File

@@ -4,6 +4,7 @@ package sql
import (
"database/sql"
"regexp"
"time"
"github.com/Sirupsen/logrus"
"github.com/cockroachdb/cockroach-go/crdb"
@@ -28,6 +29,9 @@ type flavor struct {
//
// See: https://github.com/cockroachdb/docs/blob/63761c2e/_includes/app/txn-sample.go#L41-L44
executeTx func(db *sql.DB, fn func(*sql.Tx) error) error
// Does the flavor support timezones?
supportsTimezones bool
}
// A regexp with a replacement string.
@@ -69,6 +73,8 @@ var (
}
return tx.Commit()
},
supportsTimezones: true,
}
flavorSQLite3 = flavor{
@@ -80,7 +86,7 @@ var (
{matchLiteral("boolean"), "integer"},
// Translate other types.
{matchLiteral("bytea"), "blob"},
// {matchLiteral("timestamp"), "integer"},
{matchLiteral("timestamptz"), "timestamp"},
// SQLite doesn't have a "now()" method, replace with "date('now')"
{regexp.MustCompile(`\bnow\(\)`), "date('now')"},
},
@@ -107,6 +113,22 @@ func (f flavor) translate(query string) string {
return query
}
// translateArgs translates query parameters that may be unique to
// a specific SQL flavor. For example, standardizing "time.Time"
// types to UTC for clients that don't provide timezone support.
func (c *conn) translateArgs(args []interface{}) []interface{} {
if c.flavor.supportsTimezones {
return args
}
for i, arg := range args {
if t, ok := arg.(time.Time); ok {
args[i] = t.UTC()
}
}
return args
}
// conn is the main database connection.
type conn struct {
db *sql.DB
@@ -122,17 +144,17 @@ func (c *conn) Close() error {
func (c *conn) Exec(query string, args ...interface{}) (sql.Result, error) {
query = c.flavor.translate(query)
return c.db.Exec(query, args...)
return c.db.Exec(query, c.translateArgs(args)...)
}
func (c *conn) Query(query string, args ...interface{}) (*sql.Rows, error) {
query = c.flavor.translate(query)
return c.db.Query(query, args...)
return c.db.Query(query, c.translateArgs(args)...)
}
func (c *conn) QueryRow(query string, args ...interface{}) *sql.Row {
query = c.flavor.translate(query)
return c.db.QueryRow(query, args...)
return c.db.QueryRow(query, c.translateArgs(args)...)
}
// ExecTx runs a method which operates on a transaction.
@@ -163,15 +185,15 @@ type trans struct {
func (t *trans) Exec(query string, args ...interface{}) (sql.Result, error) {
query = t.c.flavor.translate(query)
return t.tx.Exec(query, args...)
return t.tx.Exec(query, t.c.translateArgs(args)...)
}
func (t *trans) Query(query string, args ...interface{}) (*sql.Rows, error) {
query = t.c.flavor.translate(query)
return t.tx.Query(query, args...)
return t.tx.Query(query, t.c.translateArgs(args)...)
}
func (t *trans) QueryRow(query string, args ...interface{}) *sql.Row {
query = t.c.flavor.translate(query)
return t.tx.QueryRow(query, args...)
return t.tx.QueryRow(query, t.c.translateArgs(args)...)
}