Add support for .pgpass file (#617)
* Add support for .pgpass file * Support password lookup in /api/connect endpoint * Restrore removed code for BuildStringFromOptions * Restructure connection string test and add extra case for pgpass * Add test for FormatURL method
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"os/user"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgpassfile"
|
||||
"github.com/sosedoff/pgweb/pkg/command"
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
@@ -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),
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,163 +10,243 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Invalid_Url(t *testing.T) {
|
||||
opts := command.Options{}
|
||||
examples := []string{
|
||||
"postgre://foobar",
|
||||
"tcp://blah",
|
||||
"foobar",
|
||||
}
|
||||
func TestBuildStringFromOptions(t *testing.T) {
|
||||
t.Run("valid url", func(t *testing.T) {
|
||||
url := "postgres://myhost/database"
|
||||
str, err := BuildStringFromOptions(command.Options{URL: url})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, url, str)
|
||||
})
|
||||
|
||||
t.Run("with sslmode param", func(t *testing.T) {
|
||||
str, err := BuildStringFromOptions(command.Options{
|
||||
URL: "postgres://myhost/database",
|
||||
SSLMode: "disable",
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "postgres://myhost/database?sslmode=disable", str)
|
||||
})
|
||||
|
||||
t.Run("sets sslmode param if not set", func(t *testing.T) {
|
||||
str, err := BuildStringFromOptions(command.Options{
|
||||
URL: "postgres://localhost/database",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "postgres://localhost/database?sslmode=disable", str)
|
||||
|
||||
str, err = BuildStringFromOptions(command.Options{
|
||||
URL: "postgres://127.0.0.1/database",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=disable", str)
|
||||
})
|
||||
|
||||
t.Run("sslmode as an option", func(t *testing.T) {
|
||||
str, err := BuildStringFromOptions(command.Options{
|
||||
URL: "postgres://localhost/database",
|
||||
SSLMode: "require",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "postgres://localhost/database?sslmode=require", str)
|
||||
|
||||
str, err = BuildStringFromOptions(command.Options{
|
||||
URL: "postgres://127.0.0.1/database",
|
||||
SSLMode: "require",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=require", str)
|
||||
})
|
||||
|
||||
t.Run("localhost and sslmode flag", func(t *testing.T) {
|
||||
str, err := BuildStringFromOptions(command.Options{
|
||||
URL: "postgres://localhost/database?sslmode=require",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "postgres://localhost/database?sslmode=require", str)
|
||||
|
||||
str, err = BuildStringFromOptions(command.Options{
|
||||
URL: "postgres://127.0.0.1/database?sslmode=require",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=require", str)
|
||||
})
|
||||
|
||||
t.Run("extended options", func(t *testing.T) {
|
||||
str, err := BuildStringFromOptions(command.Options{
|
||||
URL: "postgres://localhost/database?sslmode=require&sslcert=cert&sslkey=key&sslrootcert=ca",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "postgres://localhost/database?sslcert=cert&sslkey=key&sslmode=require&sslrootcert=ca", str)
|
||||
})
|
||||
|
||||
t.Run("from flags", func(t *testing.T) {
|
||||
str, err := BuildStringFromOptions(command.Options{
|
||||
Host: "host",
|
||||
Port: 5432,
|
||||
User: "user",
|
||||
Pass: "password",
|
||||
DbName: "db",
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "postgres://user:password@host:5432/db", str)
|
||||
})
|
||||
|
||||
t.Run("localhost", func(t *testing.T) {
|
||||
opts := command.Options{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
User: "user",
|
||||
Pass: "password",
|
||||
DbName: "db",
|
||||
}
|
||||
|
||||
for _, val := range examples {
|
||||
opts.URL = val
|
||||
str, err := BuildStringFromOptions(opts)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "postgres://user:password@localhost:5432/db?sslmode=disable", str)
|
||||
|
||||
assert.Equal(t, "", str)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Invalid URL. Valid format: postgres://user:password@host:port/db?sslmode=mode", err.Error())
|
||||
opts.Host = "127.0.0.1"
|
||||
str, err = BuildStringFromOptions(opts)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "postgres://user:password@127.0.0.1:5432/db?sslmode=disable", str)
|
||||
})
|
||||
|
||||
t.Run("localhost and ssl", func(t *testing.T) {
|
||||
opts := command.Options{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
User: "user",
|
||||
Pass: "password",
|
||||
DbName: "db",
|
||||
SSLMode: "require",
|
||||
SSLKey: "keyPath",
|
||||
SSLCert: "certPath",
|
||||
SSLRootCert: "caPath",
|
||||
}
|
||||
|
||||
str, err := BuildStringFromOptions(opts)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "postgres://user:password@localhost:5432/db?sslcert=certPath&sslkey=keyPath&sslmode=require&sslrootcert=caPath", str)
|
||||
})
|
||||
|
||||
t.Run("no user", func(t *testing.T) {
|
||||
opts := command.Options{Host: "host", Port: 5432, DbName: "db"}
|
||||
u, _ := user.Current()
|
||||
str, err := BuildStringFromOptions(opts)
|
||||
userAndPass := url.UserPassword(u.Username, "").String()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, fmt.Sprintf("postgres://%s@host:5432/db", userAndPass), str)
|
||||
})
|
||||
|
||||
t.Run("port", func(t *testing.T) {
|
||||
opts := command.Options{Host: "host", User: "user", Port: 5000, DbName: "db"}
|
||||
str, err := BuildStringFromOptions(opts)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "postgres://user:@host:5000/db", str)
|
||||
})
|
||||
|
||||
t.Run("with pgpass", func(t *testing.T) {
|
||||
opts := command.Options{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
User: "username",
|
||||
DbName: "dbname",
|
||||
Passfile: "../../data/passfile",
|
||||
}
|
||||
|
||||
str, err := BuildStringFromOptions(opts)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "postgres://username:password@localhost:5432/dbname?sslmode=disable", str)
|
||||
|
||||
opts.User = "foobar"
|
||||
str, err = BuildStringFromOptions(opts)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "postgres://foobar:@localhost:5432/dbname?sslmode=disable", str)
|
||||
|
||||
opts.Host = "127.0.0.1"
|
||||
opts.DbName = "foobar2"
|
||||
str, err = BuildStringFromOptions(opts)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "postgres://foobar:password2@127.0.0.1:5432/foobar2?sslmode=disable", str)
|
||||
})
|
||||
|
||||
t.Run("invalid url", func(t *testing.T) {
|
||||
opts := command.Options{}
|
||||
examples := []string{
|
||||
"postgre://foobar",
|
||||
"tcp://blah",
|
||||
"foobar",
|
||||
}
|
||||
|
||||
for _, val := range examples {
|
||||
opts.URL = val
|
||||
str, err := BuildStringFromOptions(opts)
|
||||
|
||||
assert.Equal(t, "", str)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Invalid URL. Valid format: postgres://user:password@host:port/db?sslmode=mode", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormatURL(t *testing.T) {
|
||||
examples := []struct {
|
||||
name string
|
||||
input command.Options
|
||||
result string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "empty opts",
|
||||
input: command.Options{},
|
||||
},
|
||||
{
|
||||
name: "invalid url",
|
||||
input: command.Options{URL: "barurl"},
|
||||
err: "Invalid URL",
|
||||
},
|
||||
{
|
||||
name: "good",
|
||||
input: command.Options{
|
||||
URL: "postgres://user:pass@localhost:5432/dbname",
|
||||
},
|
||||
result: "postgres://user:pass@localhost:5432/dbname?sslmode=disable",
|
||||
},
|
||||
{
|
||||
name: "password lookup, password set",
|
||||
input: command.Options{
|
||||
URL: "postgres://username:@localhost:5432/dbname",
|
||||
Passfile: "../../data/passfile",
|
||||
},
|
||||
result: "postgres://username:password@localhost:5432/dbname?sslmode=disable",
|
||||
},
|
||||
{
|
||||
name: "password lookup, password not set",
|
||||
input: command.Options{
|
||||
URL: "postgres://username@localhost:5432/dbname",
|
||||
Passfile: "../../data/passfile",
|
||||
},
|
||||
result: "postgres://username:password@localhost:5432/dbname?sslmode=disable",
|
||||
},
|
||||
}
|
||||
|
||||
for _, ex := range examples {
|
||||
t.Run(ex.name, func(t *testing.T) {
|
||||
str, err := FormatURL(ex.input)
|
||||
|
||||
if ex.err != "" {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), ex.err)
|
||||
}
|
||||
assert.Equal(t, ex.result, str)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Valid_Url(t *testing.T) {
|
||||
url := "postgres://myhost/database"
|
||||
str, err := BuildStringFromOptions(command.Options{URL: url})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, url, str)
|
||||
}
|
||||
|
||||
func Test_Url_And_Ssl_Flag(t *testing.T) {
|
||||
str, err := BuildStringFromOptions(command.Options{
|
||||
URL: "postgres://myhost/database",
|
||||
SSLMode: "disable",
|
||||
})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://myhost/database?sslmode=disable", str)
|
||||
}
|
||||
|
||||
func Test_Localhost_Url_And_No_Ssl_Flag(t *testing.T) {
|
||||
str, err := BuildStringFromOptions(command.Options{
|
||||
URL: "postgres://localhost/database",
|
||||
})
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://localhost/database?sslmode=disable", str)
|
||||
|
||||
str, err = BuildStringFromOptions(command.Options{
|
||||
URL: "postgres://127.0.0.1/database",
|
||||
})
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=disable", str)
|
||||
}
|
||||
|
||||
func Test_Localhost_Url_And_Ssl_Flag(t *testing.T) {
|
||||
str, err := BuildStringFromOptions(command.Options{
|
||||
URL: "postgres://localhost/database",
|
||||
SSLMode: "require",
|
||||
})
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://localhost/database?sslmode=require", str)
|
||||
|
||||
str, err = BuildStringFromOptions(command.Options{
|
||||
URL: "postgres://127.0.0.1/database",
|
||||
SSLMode: "require",
|
||||
})
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=require", str)
|
||||
}
|
||||
|
||||
func Test_Localhost_Url_And_Ssl_Arg(t *testing.T) {
|
||||
str, err := BuildStringFromOptions(command.Options{
|
||||
URL: "postgres://localhost/database?sslmode=require",
|
||||
})
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://localhost/database?sslmode=require", str)
|
||||
|
||||
str, err = BuildStringFromOptions(command.Options{
|
||||
URL: "postgres://127.0.0.1/database?sslmode=require",
|
||||
})
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=require", str)
|
||||
}
|
||||
|
||||
func Test_ExtendedSSLFlags(t *testing.T) {
|
||||
str, err := BuildStringFromOptions(command.Options{
|
||||
URL: "postgres://localhost/database?sslmode=require&sslcert=cert&sslkey=key&sslrootcert=ca",
|
||||
})
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://localhost/database?sslcert=cert&sslkey=key&sslmode=require&sslrootcert=ca", str)
|
||||
}
|
||||
|
||||
func Test_Flag_Args(t *testing.T) {
|
||||
str, err := BuildStringFromOptions(command.Options{
|
||||
Host: "host",
|
||||
Port: 5432,
|
||||
User: "user",
|
||||
Pass: "password",
|
||||
DbName: "db",
|
||||
})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://user:password@host:5432/db", str)
|
||||
}
|
||||
|
||||
func Test_Localhost(t *testing.T) {
|
||||
opts := command.Options{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
User: "user",
|
||||
Pass: "password",
|
||||
DbName: "db",
|
||||
}
|
||||
|
||||
str, err := BuildStringFromOptions(opts)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://user:password@localhost:5432/db?sslmode=disable", str)
|
||||
|
||||
opts.Host = "127.0.0.1"
|
||||
str, err = BuildStringFromOptions(opts)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://user:password@127.0.0.1:5432/db?sslmode=disable", str)
|
||||
}
|
||||
|
||||
func Test_Localhost_And_Ssl(t *testing.T) {
|
||||
opts := command.Options{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
User: "user",
|
||||
Pass: "password",
|
||||
DbName: "db",
|
||||
SSLMode: "require",
|
||||
SSLKey: "keyPath",
|
||||
SSLCert: "certPath",
|
||||
SSLRootCert: "caPath",
|
||||
}
|
||||
|
||||
str, err := BuildStringFromOptions(opts)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://user:password@localhost:5432/db?sslcert=certPath&sslkey=keyPath&sslmode=require&sslrootcert=caPath", str)
|
||||
}
|
||||
|
||||
func Test_No_User(t *testing.T) {
|
||||
opts := command.Options{Host: "host", Port: 5432, DbName: "db"}
|
||||
u, _ := user.Current()
|
||||
str, err := BuildStringFromOptions(opts)
|
||||
userAndPass := url.UserPassword(u.Username, "").String()
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, fmt.Sprintf("postgres://%s@host:5432/db", userAndPass), str)
|
||||
}
|
||||
|
||||
func Test_Port(t *testing.T) {
|
||||
opts := command.Options{Host: "host", User: "user", Port: 5000, DbName: "db"}
|
||||
str, err := BuildStringFromOptions(opts)
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://user:@host:5000/db", str)
|
||||
}
|
||||
|
||||
func Test_Blank(t *testing.T) {
|
||||
func TestIsBlank(t *testing.T) {
|
||||
assert.Equal(t, true, IsBlank(command.Options{}))
|
||||
assert.Equal(t, false, IsBlank(command.Options{Host: "host", User: "user"}))
|
||||
assert.Equal(t, false, IsBlank(command.Options{Host: "host", User: "user", DbName: "db"}))
|
||||
|
||||
Reference in New Issue
Block a user