diff --git a/main.go b/main.go index c3cd91e..444a908 100644 --- a/main.go +++ b/main.go @@ -64,6 +64,15 @@ func initOptions() { os.Exit(0) } + if options.ReadOnly { + msg := `------------------------------------------------------ +SECURITY WARNING: You are running pgweb in read-only mode. +This mode is designed for environments where users could potentially delete / change data. +For proper read-only access please follow postgresql role management documentation. +------------------------------------------------------` + fmt.Println(msg) + } + printVersion() } diff --git a/pkg/client/client.go b/pkg/client/client.go index 223c06b..5c3e274 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -225,7 +225,29 @@ func (client *Client) Query(query string) (*Result, error) { return res, err } +func (client *Client) SetReadOnlyMode() error { + var value string + if err := client.db.Get(&value, "SHOW default_transaction_read_only;"); err != nil { + return err + } + + if value == "off" { + _, err := client.db.Exec("SET default_transaction_read_only=on;") + return err + } + + return nil +} + func (client *Client) query(query string, args ...interface{}) (*Result, error) { + // We're going to force-set transaction mode on every query. + // This is needed so that default mode could not be changed by user. + if command.Opts.ReadOnly { + if err := client.SetReadOnlyMode(); err != nil { + return nil, err + } + } + action := strings.ToLower(strings.Split(query, " ")[0]) if action == "update" || action == "delete" { res, err := client.db.Exec(query, args...) diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index bec6544..2c4e935 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -333,6 +333,18 @@ func test_HistoryUniqueness(t *testing.T) { assert.Equal(t, "SELECT * FROM books WHERE id = 1", client.History[0].Query) } +func test_ReadOnlyMode(t *testing.T) { + url := fmt.Sprintf("postgres://%s@%s:%s/%s?sslmode=disable", serverUser, serverHost, serverPort, serverDatabase) + client, _ := NewFromUrl(url, nil) + + err := client.SetReadOnlyMode() + assert.Equal(t, nil, err) + + _, err = client.Query("CREATE TABLE foobar(id integer);") + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "in a read-only transaction") +} + func TestAll(t *testing.T) { if onWindows() { t.Log("Unit testing on Windows platform is not supported.") @@ -361,7 +373,9 @@ func TestAll(t *testing.T) { test_TableRowsOrderEscape(t) test_ResultCsv(t) test_History(t) + test_HistoryUniqueness(t) test_HistoryError(t) + test_ReadOnlyMode(t) teardownClient() teardown() diff --git a/pkg/command/options.go b/pkg/command/options.go index d08c2c6..b2c8435 100644 --- a/pkg/command/options.go +++ b/pkg/command/options.go @@ -24,6 +24,7 @@ type Options struct { SkipOpen bool `short:"s" long:"skip-open" description:"Skip browser open on start"` Sessions bool `long:"sessions" description:"Enable multiple database sessions" default:"false"` Prefix string `long:"prefix" description:"Add a url prefix"` + ReadOnly bool `long:"readonly" description:"Run database connection in readonly mode"` } var Opts Options