Automatically create the HMN user if necessary

This pretty much certainly won't work in real environments. If it does,
your db config is not secure :)
This commit is contained in:
Ben Visness 2022-05-11 19:20:22 -05:00
parent f4601198c9
commit e4bb741a15
2 changed files with 95 additions and 18 deletions

View File

@ -14,7 +14,7 @@ var Config = HMNConfig{
Postgres: PostgresConfig{ Postgres: PostgresConfig{
User: "hmn", User: "hmn",
Password: "password", Password: "password",
Hostname: "handmade.local", Hostname: "localhost",
Port: 5432, Port: 5432,
DbName: "hmn", DbName: "hmn",
LogLevel: pgx.LogLevelTrace, // LogLevelWarn is recommended for production LogLevel: pgx.LogLevelTrace, // LogLevelWarn is recommended for production

View File

@ -5,6 +5,7 @@ import (
_ "embed" _ "embed"
"fmt" "fmt"
"os" "os"
"os/user"
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
@ -319,27 +320,81 @@ func ResetDB() {
fmt.Println("Resetting database...") fmt.Println("Resetting database...")
ctx := context.Background() ctx := context.Background()
// NOTE(asaf): We connect to db "template1", because we have to connect to something other than our own db in order to drop it.
template1DSN := fmt.Sprintf("user=%s password=%s host=%s port=%d dbname=%s", // Create the HMN database user
config.Config.Postgres.User, {
config.Config.Postgres.Password, type pgCredentials struct {
config.Config.Postgres.Hostname, User string
config.Config.Postgres.Port, Password string
"template1", // NOTE(asaf): template1 must always exist in postgres, as it's the db that gets cloned when you create new DBs }
) credentials := []pgCredentials{
// NOTE(asaf): We have to use the low-level API of pgconn, because the pgx Exec always wraps the query in a transaction. {config.Config.Postgres.User, config.Config.Postgres.Password}, // Existing HMN user
lowLevelConn, err := pgconn.Connect(ctx, template1DSN) {getSystemUsername(), ""}, // Postgres.app on Mac
}
var workingCred pgCredentials
var createUserConn *pgconn.PgConn
var connErrors []error
for _, cred := range credentials {
// NOTE(asaf): We have to use the low-level API of pgconn, because the pgx Exec always wraps the query in a transaction.
var err error
createUserConn, err = connectLowLevel(ctx, cred.User, cred.Password)
if err == nil {
workingCred = cred
break
} else {
connErrors = append(connErrors, err)
}
}
if createUserConn == nil {
fmt.Println("Failed to connect to the db to reset it.")
fmt.Println("The following errors occurred for each set of credentials:")
for _, err := range connErrors {
fmt.Printf("- %v\n", err)
}
fmt.Println()
fmt.Println("If this is a local development environment, please let us know what platform you are")
fmt.Println("using and how you installed Postgres. We can make sure your default Postgres username")
fmt.Println("and password are added to our list to avoid this problem in the future.")
fmt.Println()
fmt.Println("If on the other hand this is a real deployment, please manually create the user:")
fmt.Println()
fmt.Println(" CREATE USER <username> WITH ENCRYPTED PASSWORD '<password>';")
fmt.Println()
fmt.Println("and add the username and password to your config.")
os.Exit(1)
}
defer createUserConn.Close(ctx)
// Create the HMN user
{
userExists := workingCred.User == config.Config.Postgres.User && workingCred.Password == config.Config.Postgres.Password
if !userExists {
result := createUserConn.ExecParams(ctx, fmt.Sprintf(`
CREATE USER %s WITH
ENCRYPTED PASSWORD '%s'
CREATEDB
`, config.Config.Postgres.User, config.Config.Postgres.Password), nil, nil, nil, nil)
_, err := result.Close()
if err != nil {
panic(fmt.Errorf("failed to create HMN user: %w", err))
}
}
}
}
// Connect as the HMN user
conn, err := connectLowLevel(ctx, config.Config.Postgres.User, config.Config.Postgres.Password)
if err != nil { if err != nil {
panic(fmt.Errorf("failed to connect to db: %w", err)) panic(fmt.Errorf("failed to connect to db: %w", err))
} }
defer lowLevelConn.Close(ctx)
// Disconnect all other users // Disconnect all other users
{ {
result := lowLevelConn.ExecParams(ctx, fmt.Sprintf(` result := conn.ExecParams(ctx, fmt.Sprintf(`
SELECT pg_terminate_backend(pid) SELECT pg_terminate_backend(pid)
FROM pg_stat_activity FROM pg_stat_activity
WHERE datname = '%s' AND pid <> pg_backend_pid() WHERE datname IN ('%s', 'template1') AND pid <> pg_backend_pid()
`, config.Config.Postgres.DbName), nil, nil, nil, nil) `, config.Config.Postgres.DbName), nil, nil, nil, nil)
_, err := result.Close() _, err := result.Close()
if err != nil { if err != nil {
@ -349,8 +404,8 @@ func ResetDB() {
// Drop the database // Drop the database
{ {
result := lowLevelConn.ExecParams(ctx, fmt.Sprintf("DROP DATABASE %s", config.Config.Postgres.DbName), nil, nil, nil, nil) result := conn.ExecParams(ctx, fmt.Sprintf("DROP DATABASE %s", config.Config.Postgres.DbName), nil, nil, nil, nil)
_, err = result.Close() _, err := result.Close()
pgErr, isPgError := err.(*pgconn.PgError) pgErr, isPgError := err.(*pgconn.PgError)
if err != nil { if err != nil {
if !(isPgError && pgErr.SQLState() == "3D000") { // NOTE(asaf): 3D000 means "Database does not exist" if !(isPgError && pgErr.SQLState() == "3D000") { // NOTE(asaf): 3D000 means "Database does not exist"
@ -361,10 +416,32 @@ func ResetDB() {
// Create the database again // Create the database again
{ {
result := lowLevelConn.ExecParams(ctx, fmt.Sprintf("CREATE DATABASE %s", config.Config.Postgres.DbName), nil, nil, nil, nil) result := conn.ExecParams(ctx, fmt.Sprintf("CREATE DATABASE %s", config.Config.Postgres.DbName), nil, nil, nil, nil)
_, err = result.Close() _, err := result.Close()
if err != nil { if err != nil {
panic(fmt.Errorf("failed to create db: %w", err)) panic(fmt.Errorf("failed to create db: %w", err))
} }
} }
fmt.Println("Database reset successfully.")
}
func connectLowLevel(ctx context.Context, username, password string) (*pgconn.PgConn, error) {
// NOTE(asaf): We connect to db "template1", because we have to connect to something other than our own db in order to drop it.
template1DSN := fmt.Sprintf("user=%s password=%s host=%s port=%d dbname=%s",
username,
password,
config.Config.Postgres.Hostname,
config.Config.Postgres.Port,
"template1", // NOTE(asaf): template1 must always exist in postgres, as it's the db that gets cloned when you create new DBs
)
return pgconn.Connect(ctx, template1DSN)
}
func getSystemUsername() string {
u, err := user.Current()
if err != nil {
return ""
}
return u.Username
} }