Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for .pgpass file #617

Merged
merged 5 commits into from
Dec 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions data/passfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
localhost:5432:dbname:username:password
127.0.0.1:5432:*:*:password2
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ require (
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.9.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
Expand Down
10 changes: 5 additions & 5 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,22 +142,22 @@ func Connect(c *gin.Context) {
return
}

var sshInfo *shared.SSHInfo
url := c.Request.FormValue("url")

if url == "" {
badRequest(c, errURLRequired)
return
}

opts := command.Options{URL: url}
url, err := connection.FormatURL(opts)

url, err := connection.FormatURL(command.Options{
URL: url,
Passfile: command.Opts.Passfile,
})
if err != nil {
badRequest(c, err)
return
}

var sshInfo *shared.SSHInfo
if c.Request.FormValue("ssh") != "" {
sshInfo = parseSshInfo(c)
}
Expand Down
20 changes: 20 additions & 0 deletions pkg/command/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import (
"fmt"
"os"
"os/user"
"path/filepath"
"strings"

"github.com/jackc/pgpassfile"
"github.com/jessevdk/go-flags"
"github.com/sirupsen/logrus"
)
Expand All @@ -26,6 +28,7 @@ type Options struct {
Port int `long:"port" description:"Server port" default:"5432"`
User string `long:"user" description:"Database user"`
Pass string `long:"pass" description:"Password for user"`
Passfile string `long:"passfile" description:"Local passwords file location"`
DbName string `long:"db" description:"Database name"`
SSLMode string `long:"ssl" description:"SSL mode"`
SSLRootCert string `long:"ssl-rootcert" description:"SSL certificate authority file"`
Expand Down Expand Up @@ -79,6 +82,23 @@ func ParseOptions(args []string) (Options, error) {
opts.Prefix = getPrefixedEnvVar("URL_PREFIX")
}

if opts.Passfile == "" {
passfile := os.Getenv("PGPASSFILE")
if passfile == "" {
passfile = filepath.Join(os.Getenv("HOME"), ".pgpass")
}

_, err := os.Stat(passfile)
if err == nil {
_, err = pgpassfile.ReadPassfile(passfile)
if err == nil {
opts.Passfile = passfile
} else {
fmt.Printf("[WARN] Pgpass file unreadable: %s\n", err)
}
}
}

// Handle edge case where pgweb is started with a default host `localhost` and no user.
// When user is not set the `lib/pq` connection will fail and cause pgweb's termination.
if (opts.Host == "localhost" || opts.Host == "127.0.0.1") && opts.User == "" {
Expand Down
102 changes: 65 additions & 37 deletions pkg/command/options_test.go
Original file line number Diff line number Diff line change
@@ -1,47 +1,75 @@
package command

import (
"os"
"testing"

"github.com/stretchr/testify/assert"
)

func TestParseOptions(t *testing.T) {
// Test default behavior
opts, err := ParseOptions([]string{})
assert.NoError(t, err)
assert.Equal(t, false, opts.Sessions)
assert.Equal(t, "", opts.Prefix)
assert.Equal(t, "", opts.ConnectToken)
assert.Equal(t, "", opts.ConnectHeaders)
assert.Equal(t, false, opts.DisableSSH)
assert.Equal(t, false, opts.DisablePrettyJSON)
assert.Equal(t, false, opts.DisableConnectionIdleTimeout)
assert.Equal(t, 180, opts.ConnectionIdleTimeout)
assert.Equal(t, false, opts.Cors)
assert.Equal(t, "*", opts.CorsOrigin)

// Test sessions
opts, err = ParseOptions([]string{"--sessions", "1"})
assert.NoError(t, err)
assert.Equal(t, true, opts.Sessions)

// Test url prefix
opts, err = ParseOptions([]string{"--prefix", "pgweb"})
assert.NoError(t, err)
assert.Equal(t, "pgweb/", opts.Prefix)

opts, err = ParseOptions([]string{"--prefix", "pgweb/"})
assert.NoError(t, err)
assert.Equal(t, "pgweb/", opts.Prefix)

// Test connect backend options
opts, err = ParseOptions([]string{"--connect-backend", "test"})
assert.EqualError(t, err, "--sessions flag must be set")

opts, err = ParseOptions([]string{"--connect-backend", "test", "--sessions"})
assert.EqualError(t, err, "--connect-token flag must be set")

opts, err = ParseOptions([]string{"--connect-backend", "test", "--sessions", "--connect-token", "token"})
assert.NoError(t, err)
t.Run("defaults", func(t *testing.T) {
opts, err := ParseOptions([]string{})
assert.NoError(t, err)
assert.Equal(t, false, opts.Sessions)
assert.Equal(t, "", opts.Prefix)
assert.Equal(t, "", opts.ConnectToken)
assert.Equal(t, "", opts.ConnectHeaders)
assert.Equal(t, false, opts.DisableSSH)
assert.Equal(t, false, opts.DisablePrettyJSON)
assert.Equal(t, false, opts.DisableConnectionIdleTimeout)
assert.Equal(t, 180, opts.ConnectionIdleTimeout)
assert.Equal(t, false, opts.Cors)
assert.Equal(t, "*", opts.CorsOrigin)
assert.Equal(t, "", opts.Passfile)
})

t.Run("sessions", func(t *testing.T) {
opts, err := ParseOptions([]string{"--sessions", "1"})
assert.NoError(t, err)
assert.Equal(t, true, opts.Sessions)
})

t.Run("url prefix", func(t *testing.T) {
opts, err := ParseOptions([]string{"--prefix", "pgweb"})
assert.NoError(t, err)
assert.Equal(t, "pgweb/", opts.Prefix)

opts, err = ParseOptions([]string{"--prefix", "pgweb/"})
assert.NoError(t, err)
assert.Equal(t, "pgweb/", opts.Prefix)
})

t.Run("connect backend", func(t *testing.T) {
_, err := ParseOptions([]string{"--connect-backend", "test"})
assert.EqualError(t, err, "--sessions flag must be set")

_, err = ParseOptions([]string{"--connect-backend", "test", "--sessions"})
assert.EqualError(t, err, "--connect-token flag must be set")

_, err = ParseOptions([]string{"--connect-backend", "test", "--sessions", "--connect-token", "token"})
assert.NoError(t, err)
})

t.Run("passfile", func(t *testing.T) {
defer os.Unsetenv("PGPASSFILE")

// File does not exist
os.Setenv("PGPASSFILE", "/tmp/foo")
opts, err := ParseOptions([]string{})
assert.NoError(t, err)
assert.Equal(t, "", opts.Passfile)

// File exists and valid
os.Setenv("PGPASSFILE", "../../data/passfile")
opts, err = ParseOptions([]string{})
assert.NoError(t, err)
assert.Equal(t, "../../data/passfile", opts.Passfile)

// Set via flag
os.Unsetenv("PGPASSFILE")
opts, err = ParseOptions([]string{"--passfile", "../../data/passfile"})
assert.NoError(t, err)
assert.Equal(t, "../../data/passfile", opts.Passfile)
})
}
48 changes: 48 additions & 0 deletions pkg/connection/connection_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os/user"
"strings"

"github.com/jackc/pgpassfile"
"github.com/sosedoff/pgweb/pkg/command"
)

Expand Down Expand Up @@ -76,6 +77,17 @@ func FormatURL(opts command.Options) (string, error) {
}
}

// When password is not provided, look it up from a .pgpass file
if uri.User != nil {
pass, _ := uri.User.Password()
if pass == "" && opts.Passfile != "" {
pass = lookupPassword(opts, uri)
if pass != "" {
uri.User = neturl.UserPassword(uri.User.Username(), pass)
}
}
}

// Rebuild query params
query := neturl.Values{}
for k, v := range params {
Expand Down Expand Up @@ -125,6 +137,11 @@ func BuildStringFromOptions(opts command.Options) (string, error) {
query.Add("sslrootcert", opts.SSLRootCert)
}

// Grab password from .pgpass file if it's available
if opts.Pass == "" && opts.Passfile != "" {
opts.Pass = lookupPassword(opts, nil)
}

url := neturl.URL{
Scheme: "postgres",
Host: fmt.Sprintf("%v:%v", opts.Host, opts.Port),
Expand All @@ -135,3 +152,34 @@ func BuildStringFromOptions(opts command.Options) (string, error) {

return url.String(), nil
}

func lookupPassword(opts command.Options, url *neturl.URL) string {
if opts.Passfile == "" {
return ""
}

passfile, err := pgpassfile.ReadPassfile(opts.Passfile)
if err != nil {
fmt.Println("[WARN] .pgpassfile", opts.Passfile, "is not readable")
return ""
}

if url != nil {
var dbName string
fmt.Sscanf(url.Path, "/%s", &dbName)

return passfile.FindPassword(
url.Hostname(),
url.Port(),
dbName,
url.User.Username(),
)
}

return passfile.FindPassword(
opts.Host,
fmt.Sprintf("%d", opts.Port),
opts.DbName,
opts.User,
)
}
Loading