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 runAsUser functionality #81

Closed
wants to merge 1 commit into from
Closed
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
Add runAsUser functionality
By supplying runAsUser it is possible to run initdb as non-root user
(which otherwise fails) even if the calling process runs as root.

In addition, flush logger on failures to get more detailed errors.
  • Loading branch information
same-id committed Jul 26, 2022
commit 93d5956f28fa2e0b33532b39a65b2e72147b33df
7 changes: 7 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type Config struct {
version PostgresVersion
port uint32
database string
runAsUser string
username string
password string
runtimePath string
Expand Down Expand Up @@ -61,6 +62,12 @@ func (c Config) Database(database string) Config {
return c
}

// RunAsUser sets the user that invoke the initdb command.
func (c Config) RunAsUser(runAsUser string) Config {
c.runAsUser = runAsUser
return c
}

// Username sets the username that will be used to connect.
func (c Config) Username(username string) Config {
c.username = username
Expand Down
19 changes: 18 additions & 1 deletion embedded_postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,13 @@ func (ep *EmbeddedPostgres) Start() error {

if !reuseData {
if err := ep.cleanDataDirectoryAndInit(); err != nil {
ep.syncedLogger.flush()
return err
}
}

if err := startPostgres(ep); err != nil {
ep.syncedLogger.flush()
return err
}

Expand Down Expand Up @@ -153,7 +155,7 @@ func (ep *EmbeddedPostgres) cleanDataDirectoryAndInit() error {
return fmt.Errorf("unable to clean up data directory %s with error: %s", ep.config.dataPath, err)
}

if err := ep.initDatabase(ep.config.binariesPath, ep.config.runtimePath, ep.config.dataPath, ep.config.username, ep.config.password, ep.config.locale, ep.syncedLogger.file); err != nil {
if err := ep.initDatabase(ep.config.binariesPath, ep.config.runtimePath, ep.config.dataPath, ep.config.runAsUser, ep.config.username, ep.config.password, ep.config.locale, ep.syncedLogger.file); err != nil {
return err
}

Expand All @@ -167,6 +169,7 @@ func (ep *EmbeddedPostgres) Stop() error {
}

if err := stopPostgres(ep); err != nil {
ep.syncedLogger.flush()
return err
}

Expand All @@ -187,6 +190,13 @@ func startPostgres(ep *EmbeddedPostgres) error {
postgresProcess.Stdout = ep.syncedLogger.file
postgresProcess.Stderr = ep.syncedLogger.file

if ep.config.runAsUser != "" {
err := setRunAs(postgresProcess, ep.config.runAsUser)
if err != nil {
return err
}
}

if err := postgresProcess.Run(); err != nil {
return fmt.Errorf("could not start postgres using %s", postgresProcess.String())
}
Expand All @@ -201,6 +211,13 @@ func stopPostgres(ep *EmbeddedPostgres) error {
postgresProcess.Stderr = ep.syncedLogger.file
postgresProcess.Stdout = ep.syncedLogger.file

if ep.config.runAsUser != "" {
err := setRunAs(postgresProcess, ep.config.runAsUser)
if err != nil {
return err
}
}

if err := postgresProcess.Run(); err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions embedded_postgres_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func Test_ErrorWhenUnableToInitDatabase(t *testing.T) {
return jarFile, true
}

database.initDatabase = func(binaryExtractLocation, runtimePath, dataLocation, username, password, locale string, logger *os.File) error {
database.initDatabase = func(binaryExtractLocation, runtimePath, dataLocation, runAsUser, username, password, locale string, logger *os.File) error {
return errors.New("ah it did not work")
}

Expand Down Expand Up @@ -226,7 +226,7 @@ func Test_ErrorWhenCannotStartPostgresProcess(t *testing.T) {
return jarFile, true
}

database.initDatabase = func(binaryExtractLocation, runtimePath, dataLocation, username, password, locale string, logger *os.File) error {
database.initDatabase = func(binaryExtractLocation, runtimePath, dataLocation, runAsUser, username, password, locale string, logger *os.File) error {
return nil
}

Expand Down
21 changes: 19 additions & 2 deletions prepare_database.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ const (
fmtAfterError = "%v happened after error: %w"
)

type initDatabase func(binaryExtractLocation, runtimePath, pgDataDir, username, password, locale string, logger *os.File) error
type initDatabase func(binaryExtractLocation, runtimePath, pgDataDir, runAsUser, username, password, locale string, logger *os.File) error
type createDatabase func(port uint32, username, password, database string) error

func defaultInitDatabase(binaryExtractLocation, runtimePath, pgDataDir, username, password, locale string, logger *os.File) error {
func defaultInitDatabase(binaryExtractLocation, runtimePath, pgDataDir, runAsUser, username, password, locale string, logger *os.File) error {
passwordFile, err := createPasswordFile(runtimePath, password)
if err != nil {
return err
Expand All @@ -44,6 +44,23 @@ func defaultInitDatabase(binaryExtractLocation, runtimePath, pgDataDir, username
postgresInitDBProcess.Stderr = logger
postgresInitDBProcess.Stdout = logger

if runAsUser != "" {
err = chown(passwordFile, runAsUser)
if err != nil {
return err
}

err = chown(runtimePath, runAsUser)
if err != nil {
return err
}

err = setRunAs(postgresInitDBProcess, runAsUser)
if err != nil {
return err
}
}

if err := postgresInitDBProcess.Run(); err != nil {
return fmt.Errorf("unable to init database using: %s", postgresInitDBProcess.String())
}
Expand Down
6 changes: 3 additions & 3 deletions prepare_database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
)

func Test_defaultInitDatabase_ErrorWhenCannotCreatePasswordFile(t *testing.T) {
err := defaultInitDatabase("path_not_exists", "path_not_exists", "path_not_exists", "Tom", "Beer", "", os.Stderr)
err := defaultInitDatabase("path_not_exists", "path_not_exists", "path_not_exists", "", "Tom", "Beer", "", os.Stderr)

assert.EqualError(t, err, "unable to write password file to path_not_exists/pwfile")
}
Expand All @@ -38,7 +38,7 @@ func Test_defaultInitDatabase_ErrorWhenCannotStartInitDBProcess(t *testing.T) {
}
}()

err = defaultInitDatabase(binTempDir, runtimeTempDir, filepath.Join(runtimeTempDir, "data"), "Tom", "Beer", "", os.Stderr)
err = defaultInitDatabase(binTempDir, runtimeTempDir, filepath.Join(runtimeTempDir, "data"), "", "Tom", "Beer", "", os.Stderr)

assert.EqualError(t, err, fmt.Sprintf("unable to init database using: %s/bin/initdb -A password -U Tom -D %s/data --pwfile=%s/pwfile",
binTempDir,
Expand All @@ -59,7 +59,7 @@ func Test_defaultInitDatabase_ErrorInvalidLocaleSetting(t *testing.T) {
}
}()

err = defaultInitDatabase(tempDir, tempDir, filepath.Join(tempDir, "data"), "postgres", "postgres", "en_XY", os.Stderr)
err = defaultInitDatabase(tempDir, tempDir, filepath.Join(tempDir, "data"), "", "postgres", "postgres", "en_XY", os.Stderr)

assert.EqualError(t, err, fmt.Sprintf("unable to init database using: %s/bin/initdb -A password -U postgres -D %s/data --pwfile=%s/pwfile --locale=en_XY",
tempDir,
Expand Down
59 changes: 59 additions & 0 deletions runas.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//go:build !windows
// +build !windows

package embeddedpostgres

import (
"fmt"
"os"
"os/exec"
"os/user"
"strconv"
"syscall"
)

func lookupUser(runAsUser string) (uint32, uint32, error) {
u, err := user.Lookup(runAsUser)
if err != nil {
return 0, 0, fmt.Errorf("unable to lookup run-as user '%s': %w", runAsUser, err)
}

uid, err := strconv.ParseInt(u.Uid, 10, 32)
if err != nil {
return 0, 0, fmt.Errorf("unable to get uid of run-as user '%s': %w", runAsUser, err)
}

gid, err := strconv.ParseInt(u.Gid, 10, 32)
if err != nil {
return 0, 0, fmt.Errorf("unable to get gid of run-as user '%s': %w", runAsUser, err)
}

return uint32(uid), uint32(gid), nil
}

func setRunAs(process *exec.Cmd, runAsUser string) error {
uid, gid, err := lookupUser(runAsUser)
if err != nil {
return err
}

process.SysProcAttr = &syscall.SysProcAttr{
Credential: &syscall.Credential{Uid: uid, Gid: gid, NoSetGroups: true},
}

return nil
}

func chown(file string, runAsUser string) error {
uid, gid, err := lookupUser(runAsUser)
if err != nil {
return err
}

err = os.Chown(file, int(uid), int(gid))
if err != nil {
return fmt.Errorf("unable to chown '%s' file with '%s': %w", file, runAsUser, err)
}

return nil
}
90 changes: 90 additions & 0 deletions runas_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//go:build !windows
// +build !windows

package embeddedpostgres

import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"os/user"
"testing"

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

func Test_defaultInitDatabase_RunAsUnknownUser(t *testing.T) {
tempDir, err := ioutil.TempDir("", "prepare_database_test")
if err != nil {
panic(err)
}

defer func() {
if err := os.RemoveAll(tempDir); err != nil {
panic(err)
}
}()

database := NewDatabase(DefaultConfig().RuntimePath(tempDir).RunAsUser("+"))
err = database.Start()
assert.EqualError(t, err, "unable to lookup run-as user '+': user: unknown user +")
}

func Test_defaultInitDatabase_RunAsSameUser(t *testing.T) {
tempDir, err := ioutil.TempDir("", "prepare_database_test")
if err != nil {
panic(err)
}

defer func() {
if err := os.RemoveAll(tempDir); err != nil {
panic(err)
}
}()

currentUser, err := user.Current()
if err != nil {
t.Fatal(err)
}

// Same user
username := currentUser.Username

database := NewDatabase(DefaultConfig().RuntimePath(tempDir).RunAsUser(username))
if err := database.Start(); err != nil {
t.Fatal(err)
}

defer func() {
if err := database.Stop(); err != nil {
t.Fatal(err)
}
}()
}

func Test_RunAsUnknownUser(t *testing.T) {
process := exec.Command("bash", "-c", "whoami")
missingUser := "+"
err := setRunAs(process, "+")
assert.EqualError(t, err, fmt.Sprintf("unable to lookup run-as user '%[1]s': user: unknown user %[1]s", missingUser))
}

func Test_ChownUnknownUser(t *testing.T) {
missingUser := "+"
file := "file"
err := chown(file, missingUser)
assert.EqualError(t, err, fmt.Sprintf("unable to lookup run-as user '%[1]s': user: unknown user %[1]s", missingUser))
}

func Test_ChownMissingFile(t *testing.T) {
currentUser, err := user.Current()
if err != nil {
t.Fatal(err)
}

username := currentUser.Username
missingFile := "+"
err = chown("+", username)
assert.EqualError(t, err, fmt.Sprintf("unable to chown '%[2]s' file with '%[1]s': chown %[2]s: no such file or directory", username, missingFile))
}
18 changes: 18 additions & 0 deletions runas_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package embeddedpostgres

import (
"fmt"
"os/exec"
)

var (
errNotSupported = fmt.Errorf("RunAsUser config parameter not supported on windows")
)

func setRunAs(process *exec.Cmd, runAsUser string) error {
return errNotSupported
}

func chown(file string, runAsUser string) error {
return errNotSupported
}
30 changes: 30 additions & 0 deletions runas_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//go:build windows
// +build windows

package embeddedpostgres

import (
"io/ioutil"
"os"
"os/user"
"testing"

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

func Test_defaultInitDatabase_RunAsNotSupported(t *testing.T) {
tempDir, err := ioutil.TempDir("", "prepare_database_test")
if err != nil {
panic(err)
}

defer func() {
if err := os.RemoveAll(tempDir); err != nil {
panic(err)
}
}()

database := NewDatabase(DefaultConfig().RuntimePath(tempDir).RunAsUser("user"))
err = database.Start()
assert.EqualError(t, err, "runAsUser config parameter not supported on windows")
}